diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ab6467a..c962be9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,5 +19,7 @@ jobs: cache: pip - name: install run: pip3 install --quiet --editable . ruff + - name: unit test + run: python3 -m unittest discover --verbose - name: ruff run: ruff check --output-format=github . diff --git a/.gitignore b/.gitignore index 68bd003..7306741 100644 --- a/.gitignore +++ b/.gitignore @@ -4,221 +4,12 @@ tanco.sdb # private key for server tanco_auth_key.* -# Byte-compiled / optimized / DLL files __pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so # Distribution / packaging -.Python build/ -develop-eggs/ dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ sdist/ -var/ wheels/ -share/python-wheels/ *.egg-info/ -.installed.cfg *.egg -MANIFEST - -# 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/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser diff --git a/tanco/client.py b/tanco/client.py index 8fd46d9..d4975a3 100644 --- a/tanco/client.py +++ b/tanco/client.py @@ -30,7 +30,7 @@ def list_challenges(self): res = requests.get(self.url + 'c.json') return res.json() - def attempt(self, challenge_name): + def attempt(self, challenge_name: str): who = self.whoami() if not who: raise LookupError('You must be logged in to attempt a challenge.') diff --git a/tanco/database.py b/tanco/database.py index dd7ca3a..01e3c6c 100644 --- a/tanco/database.py +++ b/tanco/database.py @@ -1,18 +1,20 @@ import os +import pathlib import sqlite3 from . import model as m -SDB_PATH = os.environ.get('TANCO_SDB_PATH') -if not SDB_PATH: - SDB_PATH = os.path.expanduser('~/.tanco.sdb') +if 'TANCO_SDB_PATH' in os.environ: + SDB_PATH = pathlib.Path(os.environ['TANCO_SDB_PATH']) +else: + SDB_PATH = pathlib.Path('~/.tanco.sdb').expanduser() def ensure_sdb(): - if not os.path.exists(SDB_PATH): + if not SDB_PATH.exists(): print('Creating database at', SDB_PATH) import tanco - sql = open(os.path.join(*tanco.__path__, 'sql', 'init.sql')).read() + sql = pathlib.Path(*tanco.__path__, 'sql', 'init.sql').read_text() dbc = begin() dbc.executescript(sql) dbc.commit() diff --git a/tanco/driver.py b/tanco/driver.py index c4f26bd..0df64ce 100644 --- a/tanco/driver.py +++ b/tanco/driver.py @@ -36,7 +36,7 @@ def do_login(self, _arg): return sid = db.get_server_id(self.client.url) pre = self.client.get_pre_token() - webbrowser.open(self.client.url + '/auth/login?pre=' + pre) + webbrowser.open(self.client.url + 'auth/login?pre=' + pre) jwt = self.client.get_jwt(pre=pre) data = jwtlib.JWT().decode(jwt, do_verify=False) # TODO: verify uid = db.uid_from_tokendata(sid, data['authid'], data['username']) @@ -109,7 +109,7 @@ def do_challenges(self, _arg): # -- local project config --------------------------------- - def do_init(self, arg): + def do_init(self, arg: str): """Create .tanco file in current directory""" if not (who := self.client.whoami()): print('Please login first.') diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 0000000..8358fcb --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,31 @@ +import os +import pathlib +import unittest + +TESTS_PATH = pathlib.Path(__file__).parent +TANCO_SDB_PATH = TESTS_PATH / 'tanco.sdb' +os.environ['TANCO_SDB_PATH'] = str(TANCO_SDB_PATH) +TANCO_SERVER = 'fake://invalid url/' + +import tanco.client # noqa: E402 +import tanco.database # noqa: E402 + +class BasicTest(unittest.TestCase): + def setUp(self) -> None: + TANCO_SDB_PATH.unlink(missing_ok=True) + tanco.database.ensure_sdb() + with tanco.database.begin() as conn: + cur = conn.execute('insert into servers (url, name, info) values (?, ?, ?)', + (TANCO_SERVER, 'fakeserver', 'fake')) + sid = cur.lastrowid + cur = conn.execute('insert into users (sid, authid, username) values (?, ?, ?)', + (sid, 'fakeauthid', 'fakeuser')) + uid = cur.lastrowid + conn.execute('insert into tokens (uid, jwt) values (?, ?)', (uid, 'fakejwt')) + + def tearDown(self) -> None: + TANCO_SDB_PATH.unlink() + + def test_auth(self) -> None: + client = tanco.client.TancoClient(TANCO_SERVER) + assert client.whoami()['username'] == 'fakeuser'