1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-09-19 10:51:04 +00:00

Rewriting the CLI using click rather than cmd+argparse

This commit is contained in:
Alban Féron 2021-11-11 16:47:37 +01:00
parent 8e33b374fe
commit f4bfc735e8
No known key found for this signature in database
GPG Key ID: 8CE0313646D16165
6 changed files with 355 additions and 432 deletions

View File

@ -13,6 +13,7 @@ Supysonic folder management commands
Synopsis Synopsis
======== ========
| ``supysonic-cli folder --help``
| ``supysonic-cli folder list`` | ``supysonic-cli folder list``
| ``supysonic-cli folder add`` `name` `path` | ``supysonic-cli folder add`` `name` `path`
| ``supysonic-cli folder delete`` `name` | ``supysonic-cli folder delete`` `name`
@ -43,6 +44,10 @@ audio files are located. This allows to list, add, delete and scan the folders.
Options Options
======= =======
-h, --help
Shows help and exits. Depending on where this option appears it will either list the
available commands or display help for a specific command.
-f, --force -f, --force
Force scan of already known files even if they haven't changed. Might be Force scan of already known files even if they haven't changed. Might be
useful if an update to Supysonic adds new metadata to audio files. useful if an update to Supysonic adds new metadata to audio files.

View File

@ -13,10 +13,11 @@ Supysonic user management commands
Synopsis Synopsis
======== ========
| ``supysonic-cli user --help``
| ``supysonic-cli user list`` | ``supysonic-cli user list``
| ``supysonic-cli user add`` `user` [``--password`` `password`] [``--email`` `email`] | ``supysonic-cli user add`` `user` [``--password`` `password`] [``--email`` `email`]
| ``supysonic-cli user delete`` `user` | ``supysonic-cli user delete`` `user`
| ``supysonic-cli user changepass`` `user` `password` | ``supysonic-cli user changepass`` `user` [``--password`` `password`]
| ``supysonic-cli user setroles`` [``--admin``\|\ ``--noadmin``] [``--jukebox``\|\ ``--nojukebox``] `user` | ``supysonic-cli user setroles`` [``--admin``\|\ ``--noadmin``] [``--jukebox``\|\ ``--nojukebox``] `user`
| ``supysonic-cli user rename`` `user` `newname` | ``supysonic-cli user rename`` `user` `newname`
@ -36,7 +37,7 @@ a new user, delete an existing user, and change their password or roles.
``supysonic-cli user delete`` `user` ``supysonic-cli user delete`` `user`
Delete the user `user`. Delete the user `user`.
``supysonic-cli user changepass`` `user` [`password`] ``supysonic-cli user changepass`` `user` [``--password`` `password`]
Change the password of user `user`. Will prompt for the new password if not Change the password of user `user`. Will prompt for the new password if not
provided. provided.
@ -49,6 +50,10 @@ a new user, delete an existing user, and change their password or roles.
Options Options
======= =======
-h, --help
Shows help and exits. Depending on where this option appears it will either list the
available commands or display help for a specific command.
-p password, --password password -p password, --password password
Specify the user's password upon creation. Specify the user's password upon creation.

View File

@ -13,8 +13,8 @@ Supysonic management command line interface
Synopsis Synopsis
======== ========
| ``supysonic-cli --help``
| ``supysonic-cli`` [`subcommand`] | ``supysonic-cli`` [`subcommand`]
| ``supysonic-cli help`` [`subcommand`]
Description Description
=========== ===========
@ -35,18 +35,20 @@ The "Subsonic API" is a set of adhoc standards to browse, stream or download a
music collection over HTTP. music collection over HTTP.
The command-line interface is an interface allowing administration operations The command-line interface is an interface allowing administration operations
without the use of the web interface. If ran without arguments, without the use of the web interface.
``supysonic-cli`` will open an interactive prompt, with arguments it will run
a single command and exit. Options
=======
-h, --help
Shows the help and exits. At top level it only lists the subcommands. To
display the help of a specific subcommand, add the ``--help`` flag *after*
the said subcommand name.
Subcommands Subcommands
=========== ===========
``supysonic-cli`` has three different subcommands: ``supysonic-cli`` has two different subcommands:
``help`` [`subcommand`]
When used without argument, displays the list of available subcommands. With
an argument, shows the help and arguments for the given subcommand.
``user`` `args` ... ``user`` `args` ...
User management commands User management commands

View File

@ -5,15 +5,12 @@
# #
# Distributed under terms of the GNU AGPLv3 license. # Distributed under terms of the GNU AGPLv3 license.
import argparse import click
import cmd
import getpass
import shlex
import sys
import time import time
from click.exceptions import ClickException
from pony.orm import db_session, select from pony.orm import db_session, select
from pony.orm import ObjectNotFound from pony.orm.core import ObjectNotFound
from .config import IniConfig from .config import IniConfig
from .daemon.client import DaemonClient from .daemon.client import DaemonClient
@ -25,8 +22,8 @@ from .scanner import Scanner
class TimedProgressDisplay: class TimedProgressDisplay:
def __init__(self, stdout, interval=5): def __init__(self, interval=5):
self.__stdout = stdout self.__stdout = click.get_text_stream("stdout")
self.__interval = interval self.__interval = interval
self.__last_display = 0 self.__last_display = 0
self.__last_len = 0 self.__last_len = 0
@ -42,229 +39,168 @@ class TimedProgressDisplay:
self.__last_display = time.time() self.__last_display = time.time()
class CLIParser(argparse.ArgumentParser): @click.group()
def error(self, message): def cli():
self.print_usage(sys.stderr) """Supysonic management command line interface"""
raise RuntimeError(message) pass
class SupysonicCLI(cmd.Cmd): @cli.group()
prompt = "supysonic> " def folder():
"""Folder management commands"""
pass
def _make_do(self, command):
def method(obj, line):
try:
args = getattr(obj, command + "_parser").parse_args(shlex.split(line))
except RuntimeError as e:
self.write_error_line(str(e))
return
if hasattr(obj.__class__, command + "_subparsers"): @folder.command("list")
try: @db_session
func = getattr(obj, "{}_{}".format(command, args.action)) def folder_list():
except AttributeError: """Lists folders."""
return obj.default(line)
return func(
**{key: vars(args)[key] for key in vars(args) if key != "action"}
)
else:
try:
func = getattr(obj, command)
except AttributeError:
return obj.default(line)
return func(**vars(args))
return method click.echo("Name\t\tPath\n----\t\t----")
for f in Folder.select(lambda f: f.root):
click.echo("{: <16}{}".format(f.name, f.path))
def __init__(self, config, stderr=None, *args, **kwargs):
cmd.Cmd.__init__(self, *args, **kwargs)
if stderr is not None: @folder.command("add")
self.stderr = stderr @click.argument("name")
else: @click.argument(
self.stderr = sys.stderr "path",
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True),
)
@db_session
def folder_add(name, path):
"""Adds a folder.
self.__config = config NAME can be anything but must be unique.
self.__daemon = DaemonClient(config.DAEMON["socket"]) PATH must point to an existing readable directory on the filesystem.
# Generate do_* and help_* methods If the daemon is running it will start to listen for changes in this folder but will
for parser_name in filter( not scan files already present in the folder.
lambda attr: attr.endswith("_parser") and "_" not in attr[:-7], """
dir(self.__class__),
):
command = parser_name[:-7]
if not hasattr(self.__class__, "do_" + command):
setattr(self.__class__, "do_" + command, self._make_do(command))
if hasattr(self.__class__, "do_" + command) and not hasattr(
self.__class__, "help_" + command
):
setattr(
self.__class__,
"help_" + command,
getattr(self.__class__, parser_name).print_help,
)
if hasattr(self.__class__, command + "_subparsers"):
for action, subparser in getattr(
self.__class__, command + "_subparsers"
).choices.items():
setattr(
self, "help_{} {}".format(command, action), subparser.print_help
)
def write_line(self, line=""):
self.stdout.write(line + "\n")
def write_error_line(self, line=""):
self.stderr.write(line + "\n")
def do_EOF(self, line):
return True
do_exit = do_EOF
def default(self, line):
self.write_line("Unknown command %s" % line.split()[0])
self.do_help(None)
def postloop(self):
self.write_line()
def completedefault(self, text, line, begidx, endidx):
command = line.split()[0]
parsers = getattr(self.__class__, command + "_subparsers", None)
if not parsers:
return []
num_words = len(line[len(command) : begidx].split())
if num_words == 0:
return [a for a in parsers.choices if a.startswith(text)]
return []
folder_parser = CLIParser(prog="folder", add_help=False)
folder_subparsers = folder_parser.add_subparsers(dest="action")
folder_subparsers.add_parser("list", help="Lists folders", add_help=False)
folder_add_parser = folder_subparsers.add_parser(
"add", help="Adds a folder", add_help=False
)
folder_add_parser.add_argument("name", help="Name of the folder to add")
folder_add_parser.add_argument(
"path", help="Path to the directory pointed by the folder"
)
folder_del_parser = folder_subparsers.add_parser(
"delete", help="Deletes a folder", add_help=False
)
folder_del_parser.add_argument("name", help="Name of the folder to delete")
folder_scan_parser = folder_subparsers.add_parser(
"scan", help="Run a scan on specified folders", add_help=False
)
folder_scan_parser.add_argument(
"folders",
metavar="folder",
nargs="*",
help="Folder(s) to be scanned. If ommitted, all folders are scanned",
)
folder_scan_parser.add_argument(
"-f",
"--force",
action="store_true",
help="Force scan of already know files even if they haven't changed",
)
folder_scan_target_group = folder_scan_parser.add_mutually_exclusive_group()
folder_scan_target_group.add_argument(
"--background",
action="store_true",
help="Scan the folder(s) in the background. Requires the daemon to be running.",
)
folder_scan_target_group.add_argument(
"--foreground",
action="store_true",
help="Scan the folder(s) in the foreground, blocking the processus while the scan is running.",
)
@db_session
def folder_list(self):
self.write_line("Name\t\tPath\n----\t\t----")
self.write_line(
"\n".join(
"{: <16}{}".format(f.name, f.path)
for f in Folder.select(lambda f: f.root)
)
)
@db_session
def folder_add(self, name, path):
try: try:
FolderManager.add(name, path) FolderManager.add(name, path)
self.write_line("Folder '{}' added".format(name)) click.echo("Folder '{}' added".format(name))
except ValueError as e: except ValueError as e:
self.write_error_line(str(e)) raise ClickException(str(e)) from e
@folder.command("delete")
@click.argument("name")
@db_session
def folder_delete(name):
"""Deletes a folder.
NAME is the name of the folder to delete.
"""
@db_session
def folder_delete(self, name):
try: try:
FolderManager.delete_by_name(name) FolderManager.delete_by_name(name)
self.write_line("Deleted folder '{}'".format(name)) click.echo("Deleted folder '{}'".format(name))
except ObjectNotFound as e: except ObjectNotFound as e:
self.write_error_line(str(e)) raise ClickException("Folder '{}' does not exist.".format(name)) from e
def folder_scan(self, folders, force, background, foreground):
auto = not background and not foreground @folder.command("scan")
@click.argument(
"folder",
nargs=-1,
)
@click.option(
"-f",
"--force",
is_flag=True,
default=False,
help="Force scan of already known files even if they haven't changed",
)
@click.option(
"--background",
"mode",
flag_value="background",
help="Scan the folder(s) in the background. Requires the daemon to be running.",
)
@click.option(
"--foreground",
"mode",
flag_value="foreground",
help="Scan the folder(s) in the foreground, blocking the processus while the scan is running.",
)
@click.pass_obj
def folder_scan(config, folder, force, mode):
"""Run a scan on specified folders.
FOLDER is the name of the folder to scan. Multiple can be specified. If ommitted,
all folders are scanned.
"""
daemon = DaemonClient(config.DAEMON["socket"])
# quick and dirty shorthand calls
scan_bg = lambda: daemon.scan(folder, force)
scan_fg = lambda: _folder_scan_foreground(config, daemon, folder, force)
auto = not mode
if auto: if auto:
try: try:
self.__folder_scan_background(folders, force) scan_bg()
except DaemonUnavailableError: except DaemonUnavailableError:
self.write_error_line( click.echo(
"Couldn't connect to the daemon, scanning in foreground" "Couldn't connect to the daemon, scanning in foreground", err=True
) )
self.__folder_scan_foreground(folders, force) scan_fg()
elif background: elif mode == "background":
try: try:
self.__folder_scan_background(folders, force) scan_bg()
except DaemonUnavailableError: except DaemonUnavailableError as e:
self.write_error_line( raise ClickException(
"Couldn't connect to the daemon, please use the '--foreground' option" "Couldn't connect to the daemon, please use the '--foreground' option",
) ) from e
elif foreground: elif mode == "foreground":
self.__folder_scan_foreground(folders, force) scan_fg()
def __folder_scan_background(self, folders, force):
self.__daemon.scan(folders, force)
def __folder_scan_foreground(self, folders, force): def _folder_scan_foreground(config, daemon, folders, force):
try: try:
progress = self.__daemon.get_scanning_progress() progress = daemon.get_scanning_progress()
if progress is not None: if progress is not None:
self.write_error_line( raise ClickException(
"The daemon is currently scanning, can't start a scan now" "The daemon is currently scanning, can't start a scan now"
) )
return
except DaemonUnavailableError: except DaemonUnavailableError:
pass pass
extensions = self.__config.BASE["scanner_extensions"] extensions = config.BASE["scanner_extensions"]
if extensions: if extensions:
extensions = extensions.split(" ") extensions = extensions.split(" ")
def unwatch_folder(folder):
try:
daemon.remove_watched_folder(folder.path)
except DaemonUnavailableError:
pass
def watch_folder(folder):
try:
daemon.add_watched_folder(folder.path)
except DaemonUnavailableError:
pass
scanner = Scanner( scanner = Scanner(
force=force, force=force,
extensions=extensions, extensions=extensions,
follow_symlinks=self.__config.BASE["follow_symlinks"], follow_symlinks=config.BASE["follow_symlinks"],
progress=TimedProgressDisplay(self.stdout), progress=TimedProgressDisplay(),
on_folder_start=self.__unwatch_folder, on_folder_start=unwatch_folder,
on_folder_end=self.__watch_folder, on_folder_end=watch_folder,
) )
if folders: if folders:
fstrs = folders fstrs = folders
with db_session: with db_session:
folders = select(f.name for f in Folder if f.root and f.name in fstrs)[ folders = select(f.name for f in Folder if f.root and f.name in fstrs)[:]
:
]
notfound = set(fstrs) - set(folders) notfound = set(fstrs) - set(folders)
if notfound: if notfound:
self.write_line("No such folder(s): " + " ".join(notfound)) click.echo("No such folder(s): " + " ".join(notfound))
for folder in folders: for folder in folders:
scanner.queue_folder(folder) scanner.queue_folder(folder)
else: else:
@ -275,182 +211,161 @@ class SupysonicCLI(cmd.Cmd):
scanner.run() scanner.run()
stats = scanner.stats() stats = scanner.stats()
self.write_line("\nScanning done") click.echo("\nScanning done")
self.write_line( click.echo(
"Added: {0.artists} artists, {0.albums} albums, {0.tracks} tracks".format( "Added: {0.artists} artists, {0.albums} albums, {0.tracks} tracks".format(
stats.added stats.added
) )
) )
self.write_line( click.echo(
"Deleted: {0.artists} artists, {0.albums} albums, {0.tracks} tracks".format( "Deleted: {0.artists} artists, {0.albums} albums, {0.tracks} tracks".format(
stats.deleted stats.deleted
) )
) )
if stats.errors: if stats.errors:
self.write_line("Errors in:") click.echo("Errors in:")
for err in stats.errors: for err in stats.errors:
self.write_line("- " + err) click.echo("- " + err)
def __unwatch_folder(self, folder):
try: @cli.group("user")
self.__daemon.remove_watched_folder(folder.path) def user():
except DaemonUnavailableError: """User management commands"""
pass pass
def __watch_folder(self, folder):
try:
self.__daemon.add_watched_folder(folder.path)
except DaemonUnavailableError:
pass
user_parser = CLIParser(prog="user", add_help=False) @user.command("list")
user_subparsers = user_parser.add_subparsers(dest="action") @db_session
user_subparsers.add_parser("list", help="List users", add_help=False) def user_list():
user_add_parser = user_subparsers.add_parser( """Lists users."""
"add", help="Adds a user", add_help=False
)
user_add_parser.add_argument("name", help="Name/login of the user to add")
user_add_parser.add_argument(
"-p", "--password", help="Specifies the user's password"
)
user_add_parser.add_argument(
"-e", "--email", default="", help="Sets the user's email address"
)
user_del_parser = user_subparsers.add_parser(
"delete", help="Deletes a user", add_help=False
)
user_del_parser.add_argument("name", help="Name/login of the user to delete")
user_roles_parser = user_subparsers.add_parser(
"setroles", help="Enable/disable rights for a user", add_help=False
)
user_roles_parser.add_argument(
"name", help="Name/login of the user to grant/revoke admin rights"
)
user_roles_admin_group = user_roles_parser.add_mutually_exclusive_group()
user_roles_admin_group.add_argument(
"-A", "--admin", action="store_true", help="Grant admin rights"
)
user_roles_admin_group.add_argument(
"-a", "--noadmin", action="store_true", help="Revoke admin rights"
)
user_roles_jukebox_group = user_roles_parser.add_mutually_exclusive_group()
user_roles_jukebox_group.add_argument(
"-J", "--jukebox", action="store_true", help="Grant jukebox rights"
)
user_roles_jukebox_group.add_argument(
"-j", "--nojukebox", action="store_true", help="Revoke jukebox rights"
)
user_pass_parser = user_subparsers.add_parser(
"changepass", help="Changes a user's password", add_help=False
)
user_pass_parser.add_argument(
"name", help="Name/login of the user to which change the password"
)
user_pass_parser.add_argument("password", nargs="?", help="New password")
user_rename_parser = user_subparsers.add_parser(
"rename", help="Rename a user", add_help=False
)
user_rename_parser.add_argument("name", help="Name of the user to rename")
user_rename_parser.add_argument("newname", help="New name for the user")
@db_session click.echo("Name\t\tAdmin\tJukebox\tEmail")
def user_list(self): click.echo("----\t\t-----\t-------\t-----")
self.write_line("Name\t\tAdmin\tJukebox\tEmail") for u in User.select():
self.write_line("----\t\t-----\t-------\t-----") click.echo(
self.write_line(
"\n".join(
"{: <16}{}\t{}\t{}".format( "{: <16}{}\t{}\t{}".format(
u.name, "*" if u.admin else "", "*" if u.jukebox else "", u.mail u.name, "*" if u.admin else "", "*" if u.jukebox else "", u.mail
) )
for u in User.select()
)
) )
def _ask_password(self): # pragma: nocover
password = getpass.getpass()
confirm = getpass.getpass("Confirm password: ")
if password != confirm:
raise ValueError("Passwords don't match")
return password
@db_session @user.command("add")
def user_add(self, name, password, email): @click.argument("name")
@click.password_option("-p", "--password", help="Specifies the user's password")
@click.option("-e", "--email", default="", help="Sets the user's email address")
@db_session
def user_add(name, password, email):
"""Adds a new user.
NAME is the name (or login) of the new user.
"""
try: try:
if not password:
password = self._ask_password() # pragma: nocover
UserManager.add(name, password, mail=email) UserManager.add(name, password, mail=email)
except ValueError as e: except ValueError as e:
self.write_error_line(str(e)) raise ClickException(str(e)) from e
@user.command("delete")
@click.argument("name")
@db_session
def user_delete(name):
"""Deletes a user.
NAME is the name of the user to delete.
"""
@db_session
def user_delete(self, name):
try: try:
UserManager.delete_by_name(name) UserManager.delete_by_name(name)
self.write_line("Deleted user '{}'".format(name)) click.echo("Deleted user '{}'".format(name))
except ObjectNotFound as e: except ObjectNotFound as e:
self.write_error_line(str(e)) raise ClickException("User '{}' does not exist.".format(name)) from e
def _echo_role_change(username, name, value):
click.echo(
"{} '{}' {} rights".format("Granted" if value else "Revoked", username, name)
)
@user.command("setroles")
@click.argument("name")
@click.option(
"-A/-a", "--admin/--noadmin", default=None, help="Grant or revoke admin rights"
)
@click.option(
"-J/-j",
"--jukebox/--nojukebox",
default=None,
help="Grant or revoke jukebox rights",
)
@db_session
def user_roles(name, admin, jukebox):
"""Enable/disable rights for a user.
NAME is the login of the user to which grant or revoke rights.
"""
@db_session
def user_setroles(self, name, admin, noadmin, jukebox, nojukebox):
user = User.get(name=name) user = User.get(name=name)
if user is None: if user is None:
self.write_error_line("No such user") raise ClickException("No such user")
else:
if admin: if admin is not None:
user.admin = True user.admin = admin
self.write_line("Granted '{}' admin rights".format(name)) _echo_role_change(name, "admin", admin)
elif noadmin: if jukebox is not None:
user.admin = False user.jukebox = jukebox
self.write_line("Revoked '{}' admin rights".format(name)) _echo_role_change(name, "jukebox", jukebox)
if jukebox:
user.jukebox = True
self.write_line("Granted '{}' jukebox rights".format(name)) @user.command("changepass")
elif nojukebox: @click.argument("name")
user.jukebox = False @click.password_option("-p", "--password", help="New password")
self.write_line("Revoked '{}' jukebox rights".format(name)) @db_session
def user_changepass(name, password):
"""Changes a user's password.
NAME is the login of the user to which change the password.
"""
@db_session
def user_changepass(self, name, password):
try: try:
if not password:
password = self._ask_password() # pragma: nocover
UserManager.change_password2(name, password) UserManager.change_password2(name, password)
self.write_line("Successfully changed '{}' password".format(name)) click.echo("Successfully changed '{}' password".format(name))
except ObjectNotFound as e: except ObjectNotFound as e:
self.write_error_line(str(e)) raise ClickException("User '{}' does not exist.".format(name)) from e
@user.command("rename")
@click.argument("name")
@click.argument("newname")
@db_session
def user_rename(name, newname):
"""Renames a user.
User NAME will then be known as NEWNAME.
"""
@db_session
def user_rename(self, name, newname):
if not name or not newname: if not name or not newname:
self.write_error_line("Missing user current name or new name") raise ClickException("Missing user current name or new name")
return
if name == newname: if name == newname:
return return
user = User.get(name=name) user = User.get(name=name)
if user is None: if user is None:
self.write_error_line("No such user") raise ClickException("No such user")
return
if User.get(name=newname) is not None: if User.get(name=newname) is not None:
self.write_error_line("This name is already taken") raise ClickException("This name is already taken")
return
user.name = newname user.name = newname
self.write_line("User '{}' renamed to '{}'".format(name, newname)) click.echo("User '{}' renamed to '{}'".format(name, newname))
def main(): def main():
config = IniConfig.from_common_locations() config = IniConfig.from_common_locations()
init_database(config.BASE["database_uri"]) init_database(config.BASE["database_uri"])
cli.main(obj=config)
cli = SupysonicCLI(config)
if len(sys.argv) > 1:
cli.onecmd(" ".join(shlex.quote(arg) for arg in sys.argv[1:]))
else:
cli.cmdloop()
release_database() release_database()

View File

@ -159,7 +159,7 @@ class DaemonClient:
return c.recv().scanned return c.recv().scanned
def scan(self, folders=[], force=False): def scan(self, folders=[], force=False):
if not isinstance(folders, list): if not isinstance(folders, (list, tuple)):
raise TypeError("Expecting list, got " + str(type(folders))) raise TypeError("Expecting list, got " + str(type(folders)))
with self.__get_connection() as c: with self.__get_connection() as c:
c.send(ScannerStartCommand(folders, force)) c.send(ScannerStartCommand(folders, force))

View File

@ -10,37 +10,39 @@ import tempfile
import shlex import shlex
import unittest import unittest
from io import StringIO from click.testing import CliRunner
from pony.orm import db_session from pony.orm import db_session
from supysonic.db import Folder, User, init_database, release_database from supysonic.db import Folder, User, init_database, release_database
from supysonic.cli import SupysonicCLI from supysonic.cli import cli
from ..testbase import TestConfig from ..testbase import TestConfig
class CLITestCase(unittest.TestCase): class CLITestCase(unittest.TestCase):
""" Really basic tests. Some even don't check anything but are just there for coverage """ """Really basic tests. Some even don't check anything but are just there for coverage"""
def setUp(self): def setUp(self):
conf = TestConfig(False, False) self.__conf = TestConfig(False, False)
self.__db = tempfile.mkstemp() self.__db = tempfile.mkstemp()
conf.BASE["database_uri"] = "sqlite:///" + self.__db[1] self.__conf.BASE["database_uri"] = "sqlite:///" + self.__db[1]
init_database(conf.BASE["database_uri"]) init_database(self.__conf.BASE["database_uri"])
self.__stdout = StringIO() self.__runner = CliRunner()
self.__stderr = StringIO()
self.__cli = SupysonicCLI(conf, stdout=self.__stdout, stderr=self.__stderr)
def tearDown(self): def tearDown(self):
self.__stdout.close()
self.__stderr.close()
release_database() release_database()
os.close(self.__db[0]) os.close(self.__db[0])
os.remove(self.__db[1]) os.remove(self.__db[1])
def __add_folder(self, name, path): def __invoke(self, cmd, expect_fail=False):
self.__cli.onecmd("folder add {} {}".format(name, shlex.quote(path))) rv = self.__runner.invoke(cli, shlex.split(cmd), obj=self.__conf)
func = self.assertNotEqual if expect_fail else self.assertEqual
func(rv.exit_code, 0)
return rv
def __add_folder(self, name, path, expect_fail=False):
self.__invoke("folder add {} {}".format(name, shlex.quote(path)), expect_fail)
def test_folder_add(self): def test_folder_add(self):
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as d:
@ -54,10 +56,10 @@ class CLITestCase(unittest.TestCase):
def test_folder_add_errors(self): def test_folder_add_errors(self):
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as d:
self.__add_folder("f1", d) self.__add_folder("f1", d)
self.__add_folder("f2", d) self.__add_folder("f2", d, True)
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as d:
self.__add_folder("f1", d) self.__add_folder("f1", d, True)
self.__cli.onecmd("folder add f3 /invalid/path") self.__invoke("folder add f3 /invalid/path", True)
with db_session: with db_session:
self.assertEqual(Folder.select().count(), 1) self.assertEqual(Folder.select().count(), 1)
@ -65,8 +67,8 @@ class CLITestCase(unittest.TestCase):
def test_folder_delete(self): def test_folder_delete(self):
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as d:
self.__add_folder("tmpfolder", d) self.__add_folder("tmpfolder", d)
self.__cli.onecmd("folder delete randomfolder") self.__invoke("folder delete randomfolder", True)
self.__cli.onecmd("folder delete tmpfolder") self.__invoke("folder delete tmpfolder")
with db_session: with db_session:
self.assertEqual(Folder.select().count(), 0) self.assertEqual(Folder.select().count(), 0)
@ -74,94 +76,88 @@ class CLITestCase(unittest.TestCase):
def test_folder_list(self): def test_folder_list(self):
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as d:
self.__add_folder("tmpfolder", d) self.__add_folder("tmpfolder", d)
self.__cli.onecmd("folder list") rv = self.__invoke("folder list")
self.assertIn("tmpfolder", self.__stdout.getvalue()) self.assertIn("tmpfolder", rv.output)
self.assertIn(d, self.__stdout.getvalue()) self.assertIn(d, rv.output)
def test_folder_scan(self): def test_folder_scan(self):
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as d:
self.__add_folder("tmpfolder", d) self.__add_folder("tmpfolder", d)
with tempfile.NamedTemporaryFile(dir=d): with tempfile.NamedTemporaryFile(dir=d):
self.__cli.onecmd("folder scan") self.__invoke("folder scan")
self.__cli.onecmd("folder scan tmpfolder nonexistent") self.__invoke("folder scan tmpfolder nonexistent")
def test_user_add(self): def test_user_add(self):
self.__cli.onecmd("user add -p Alic3 alice") self.__invoke("user add -p Alic3 alice")
self.__cli.onecmd("user add -p alice alice") self.__invoke("user add -p alice alice", True)
with db_session: with db_session:
self.assertEqual(User.select().count(), 1) self.assertEqual(User.select().count(), 1)
def test_user_delete(self): def test_user_delete(self):
self.__cli.onecmd("user add -p Alic3 alice") self.__invoke("user add -p Alic3 alice")
self.__cli.onecmd("user delete alice") self.__invoke("user delete alice")
self.__cli.onecmd("user delete bob") self.__invoke("user delete bob", True)
with db_session: with db_session:
self.assertEqual(User.select().count(), 0) self.assertEqual(User.select().count(), 0)
def test_user_list(self): def test_user_list(self):
self.__cli.onecmd("user add -p Alic3 alice") self.__invoke("user add -p Alic3 alice")
self.__cli.onecmd("user list") rv = self.__invoke("user list")
self.assertIn("alice", self.__stdout.getvalue()) self.assertIn("alice", rv.output)
def test_user_setadmin(self): def test_user_setadmin(self):
self.__cli.onecmd("user add -p Alic3 alice") self.__invoke("user add -p Alic3 alice")
self.__cli.onecmd("user setroles -A alice") self.__invoke("user setroles -A alice")
self.__cli.onecmd("user setroles -A bob") self.__invoke("user setroles -A bob", True)
with db_session: with db_session:
self.assertTrue(User.get(name="alice").admin) self.assertTrue(User.get(name="alice").admin)
def test_user_unsetadmin(self): def test_user_unsetadmin(self):
self.__cli.onecmd("user add -p Alic3 alice") self.__invoke("user add -p Alic3 alice")
self.__cli.onecmd("user setroles -A alice") self.__invoke("user setroles -A alice")
self.__cli.onecmd("user setroles -a alice") self.__invoke("user setroles -a alice")
with db_session: with db_session:
self.assertFalse(User.get(name="alice").admin) self.assertFalse(User.get(name="alice").admin)
def test_user_setjukebox(self): def test_user_setjukebox(self):
self.__cli.onecmd("user add -p Alic3 alice") self.__invoke("user add -p Alic3 alice")
self.__cli.onecmd("user setroles -J alice") self.__invoke("user setroles -J alice")
with db_session: with db_session:
self.assertTrue(User.get(name="alice").jukebox) self.assertTrue(User.get(name="alice").jukebox)
def test_user_unsetjukebox(self): def test_user_unsetjukebox(self):
self.__cli.onecmd("user add -p Alic3 alice") self.__invoke("user add -p Alic3 alice")
self.__cli.onecmd("user setroles -J alice") self.__invoke("user setroles -J alice")
self.__cli.onecmd("user setroles -j alice") self.__invoke("user setroles -j alice")
with db_session: with db_session:
self.assertFalse(User.get(name="alice").jukebox) self.assertFalse(User.get(name="alice").jukebox)
def test_user_changepass(self): def test_user_changepass(self):
self.__cli.onecmd("user add -p Alic3 alice") self.__invoke("user add -p Alic3 alice")
self.__cli.onecmd("user changepass alice newpass") self.__invoke("user changepass alice -p newpass")
self.__cli.onecmd("user changepass bob B0b") self.__invoke("user changepass bob -p B0b", True)
def test_user_rename(self): def test_user_rename(self):
self.__cli.onecmd("user add -p Alic3 alice") self.__invoke("user add -p Alic3 alice")
self.__cli.onecmd("user rename alice alice") self.__invoke("user rename alice alice")
self.__cli.onecmd("user rename bob charles") self.__invoke("user rename bob charles", True)
self.__cli.onecmd("user rename alice ''") self.__invoke("user rename alice ''", True)
with db_session: with db_session:
self.assertEqual(User.select().first().name, "alice") self.assertEqual(User.select().first().name, "alice")
self.__cli.onecmd("user rename alice bob") self.__invoke("user rename alice bob")
with db_session: with db_session:
self.assertEqual(User.select().first().name, "bob") self.assertEqual(User.select().first().name, "bob")
self.__cli.onecmd("user add -p Ch4rl3s charles") self.__invoke("user add -p Ch4rl3s charles")
self.__cli.onecmd("user rename bob charles") self.__invoke("user rename bob charles", True)
with db_session: with db_session:
self.assertEqual(User.select(lambda u: u.name == "bob").count(), 1) self.assertEqual(User.select(lambda u: u.name == "bob").count(), 1)
self.assertEqual(User.select(lambda u: u.name == "charles").count(), 1) self.assertEqual(User.select(lambda u: u.name == "charles").count(), 1)
def test_other(self):
self.assertTrue(self.__cli.do_EOF(""))
self.__cli.onecmd("unknown command")
self.__cli.postloop()
self.__cli.completedefault("user", "user", 4, 4)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()