began testing, added CI

This commit is contained in:
andy 2021-02-07 18:32:07 +00:00
parent 71a59b526e
commit 0614860c63
8 changed files with 397 additions and 25 deletions

46
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Tests
on: [push, pull_request]
jobs:
build:
strategy:
fail-fast: false
matrix:
python-version: [3.8]
poetry-version: [1.1.4]
# node: [ '10', '12' ]
os: [ubuntu-20.04, ubuntu-18.04, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2 # get source
- name: Install Python 3
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry # PYTHON dependency management
uses: abatilo/actions-poetry@v2.1.0
with:
poetry-version: ${{ matrix.poetry-version }}
- name: Install Dependencies # PYTHON install dependencies
run: poetry install
# - name: Setup node # JS setup
# uses: actions/setup-node@v2
# with:
# node-version: ${{ matrix.node }}
# - name: Install Node Packages # JS install
# run: npm ci
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@master
with:
project_id: ${{ secrets.GCP_PROJECT_ID }}
service_account_key: ${{ secrets.GCP_SA_KEY }}
export_default_credentials: true
- name: Run Tests # test script
run: poetry run python -m unittest discover -s tests

View File

@ -1,6 +1,8 @@
[Music Tools](https://music.sarsoo.xyz)
==================
![Python Tests](https://github.com/sarsoo/music-tools/workflows/Tests/badge.svg)
Set of utility tools for Spotify and Last.fm.
Built on my other libraries for Spotify ([spotframework](https://github.com/Sarsoo/spotframework)), Last.fm ([fmframework](https://github.com/Sarsoo/pyfmframework)) and interfacing utility tools for the two ([spotfm](https://github.com/Sarsoo/pyfmframework)). Currently running on a suite of Google Cloud Platform services. An iOS client is currently under development [here](https://github.com/Sarsoo/Music-Tools-iOS).

View File

@ -3,6 +3,7 @@ import shutil
import os
from pathlib import Path
import sys
import subprocess
from cmd import Cmd
stage_dir = '.music-tools'
@ -187,6 +188,11 @@ class Admin(Cmd):
with open('requirements.txt', 'w') as f:
f.write("\n".join(filtered))
def test():
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = 'service.json'
subprocess.run(
['python', '-u', '-m', 'unittest', 'discover', "-s", "tests"]
)
if __name__ == '__main__':
console = Admin()

51
poetry.lock generated
View File

@ -122,8 +122,10 @@ requests = "^2.24.0"
image = []
[package.source]
type = "directory"
url = "../fmframework"
type = "git"
url = "https://github.com/Sarsoo/pyfmframework.git"
reference = "master"
resolved_reference = "bf111d06cb4ffc9dbe0b57fbf83a3c236365c2ad"
[[package]]
name = "google-api-core"
@ -149,7 +151,7 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2)"]
[[package]]
name = "google-auth"
version = "1.24.0"
version = "1.25.0"
description = "Google Authentication Library"
category = "main"
optional = false
@ -166,7 +168,7 @@ aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)"]
[[package]]
name = "google-cloud-core"
version = "1.5.0"
version = "1.6.0"
description = "Google Cloud API client core library"
category = "main"
optional = false
@ -174,6 +176,7 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
[package.dependencies]
google-api-core = ">=1.21.0,<2.0.0dev"
google-auth = ">=1.24.0,<2.0dev"
six = ">=1.12.0"
[package.extras]
@ -301,7 +304,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "jinja2"
version = "2.11.2"
version = "2.11.3"
description = "A very fast and expressive template engine."
category = "main"
optional = false
@ -384,7 +387,7 @@ toml = ">=0.7.1"
[[package]]
name = "pytz"
version = "2020.5"
version = "2021.1"
description = "World timezone definitions, modern and historical"
category = "main"
optional = false
@ -445,16 +448,18 @@ python-versions = "^3.8"
develop = false
[package.dependencies]
fmframework = "1.0.0"
spotframework = "1.0.0"
fmframework = "branch master"
spotframework = "branch master"
[package.source]
type = "directory"
url = "../spotfm"
type = "git"
url = "https://github.com/Sarsoo/spotfm.git"
reference = "master"
resolved_reference = "4713e82819b110358ec94af7a5f9919d1aca3614"
[[package]]
name = "spotframework"
version = "1.0.0"
version = "1.0.1"
description = "Spotify HTTP wrapper library"
category = "main"
optional = false
@ -467,8 +472,10 @@ requests = "^2.24.0"
tabulate = "^0.8.7"
[package.source]
type = "directory"
url = "../spotframework"
type = "git"
url = "https://github.com/Sarsoo/spotframework.git"
reference = "master"
resolved_reference = "ac8944764f1ff3c3db40b4bc7a8897a482db9434"
[[package]]
name = "tabulate"
@ -525,7 +532,7 @@ python-versions = "*"
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "bee9f68f62a9a9f5f4d7ee11e6687acb09e8aee69956ca4767b5f25f71a94bb6"
content-hash = "943b58ae3ae110c4c990bab3fa72b73b0f8a172905246839c2fe481c73919067"
[metadata.files]
astroid = [
@ -573,12 +580,12 @@ google-api-core = [
{file = "google_api_core-1.25.1-py2.py3-none-any.whl", hash = "sha256:292dd636ed381098d24b7093ccb826b2278a12d886a3fc982084069aa24a8fbb"},
]
google-auth = [
{file = "google-auth-1.24.0.tar.gz", hash = "sha256:0b0e026b412a0ad096e753907559e4bdb180d9ba9f68dd9036164db4fdc4ad2e"},
{file = "google_auth-1.24.0-py2.py3-none-any.whl", hash = "sha256:ce752cc51c31f479dbf9928435ef4b07514b20261b021c7383bee4bda646acb8"},
{file = "google-auth-1.25.0.tar.gz", hash = "sha256:514e39f4190ca972200ba33876da5a8857c5665f2b4ccc36c8b8ee21228aae80"},
{file = "google_auth-1.25.0-py2.py3-none-any.whl", hash = "sha256:008e23ed080674f69f9d2d7d80db4c2591b9bb307d136cea7b3bc129771d211d"},
]
google-cloud-core = [
{file = "google-cloud-core-1.5.0.tar.gz", hash = "sha256:1277a015f8eeb014c48f2ec094ed5368358318f1146cf49e8de389962dc19106"},
{file = "google_cloud_core-1.5.0-py2.py3-none-any.whl", hash = "sha256:99a8a15f406f53f2b11bda1f45f952a9cdfbdbba8abf40c75651019d800879f5"},
{file = "google-cloud-core-1.6.0.tar.gz", hash = "sha256:c6abb18527545379fc82efc4de75ce9a3772ccad2fc645adace593ba097cbb02"},
{file = "google_cloud_core-1.6.0-py2.py3-none-any.whl", hash = "sha256:40d9c2da2d03549b5ac3dcccf289d4f15e6d1210044c6381ce45c92913e62904"},
]
google-cloud-firestore = [
{file = "google-cloud-firestore-1.9.0.tar.gz", hash = "sha256:d8a56919a3a32c7271d1253542ec24cb13f384a726fed354fdeb2a2269f25d1c"},
@ -664,8 +671,8 @@ itsdangerous = [
{file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},
]
jinja2 = [
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
{file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"},
{file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"},
]
lazy-object-proxy = [
{file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
@ -784,8 +791,8 @@ pylint = [
{file = "pylint-2.5.3.tar.gz", hash = "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc"},
]
pytz = [
{file = "pytz-2020.5-py2.py3-none-any.whl", hash = "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4"},
{file = "pytz-2020.5.tar.gz", hash = "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"},
{file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
{file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
]
requests = [
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},

View File

@ -12,6 +12,7 @@ packages = [
[tool.poetry.scripts]
backend-dev = 'music.music:start'
test = 'admin:test'
[tool.poetry.dependencies]
python = "^3.8"
@ -23,9 +24,9 @@ google-cloud-pubsub = "^1.6.0"
google-cloud-tasks = "^1.5.0"
requests = "^2.24.0"
spotframework = { path = "../spotframework" }
fmframework = { path = "../fmframework" }
spotfm = { path = "../spotfm" }
spotframework = { git = "https://github.com/Sarsoo/spotframework.git" }
fmframework = { git = "https://github.com/Sarsoo/pyfmframework.git" }
spotfm = { git = "https://github.com/Sarsoo/spotfm.git" }
[tool.poetry.dev-dependencies]
pylint = "2.5.3"

97
tests/test_api.py Normal file
View File

@ -0,0 +1,97 @@
import os
import unittest
from unittest.mock import patch
import flask
from music.music import create_app
import music.model.playlist
class TestAPI(unittest.TestCase):
def setUp(self):
self.app = create_app()
self.app.testing = True
self.test_app = self.app.test_client()
### ROOT ###
def test_root_route_without_login(self):
response = self.test_app.get('/')
self.assertTrue(199 < response.status_code <= 299)
def test_root_route_with_login(self):
with self.test_app.session_transaction() as sess:
sess['username'] = 'test'
response = self.test_app.get('/')
self.assertTrue(299 < response.status_code <= 399)
def test_app_route_redirects_without_login(self):
response = self.test_app.get('/app')
self.assertTrue(299 < response.status_code <= 399)
def test_app_route_with_login(self):
with self.test_app.session_transaction() as sess:
sess['username'] = 'test'
response = self.test_app.get('/app')
self.assertTrue(199 < response.status_code <= 299)
### PLAYLISTS ###
def test_all_playlists_route(self):
with self.test_app.session_transaction() as sess:
sess['username'] = 'andy'
response = self.test_app.get('/api/playlists')
self.assertTrue(199 < response.status_code <= 299)
self.assertIsNotNone(response.get_json())
self.assertTrue(len(response.get_json()['playlists']) > 0)
def test_playlist_get(self):
with self.test_app.session_transaction() as sess:
sess['username'] = 'andy'
response = self.test_app.get('/api/playlist?name=RAP')
self.assertEqual(response.status_code, 200)
def test_playlist_get_no_param(self):
with self.test_app.session_transaction() as sess:
sess['username'] = 'andy'
response = self.test_app.get('/api/playlist')
self.assertEqual(response.status_code, 400)
def test_playlist_get_wrong_param(self):
with self.test_app.session_transaction() as sess:
sess['username'] = 'andy'
response = self.test_app.get('/api/playlist?name=playlist_name')
self.assertEqual(response.status_code, 404)
#TODO: patch fireo so can test delete
def test_playlist_delete_no_param(self):
with self.test_app.session_transaction() as sess:
sess['username'] = 'andy'
response = self.test_app.delete('/api/playlist')
self.assertEqual(response.status_code, 400)
def test_playlist_delete_wrong_param(self):
with self.test_app.session_transaction() as sess:
sess['username'] = 'andy'
response = self.test_app.delete('/api/playlist?name=playlist_name')
self.assertEqual(response.status_code, 404)
### USERS ###
def test_user_get(self):
with self.test_app.session_transaction() as sess:
sess['username'] = 'andy'
response = self.test_app.get('/api/user')
self.assertEqual(response.status_code, 200)
self.assertIsNotNone(response.get_json())
if __name__ == '__main__':
unittest.main()

192
tests/test_decorators.py Normal file
View File

@ -0,0 +1,192 @@
import os
import unittest
from unittest.mock import Mock
import flask
from music.music import create_app
from music.api.decorators import is_logged_in, admin_required, spotify_link_required, lastfm_username_required, check_dict, validate_json
class TestDecorators(unittest.TestCase):
def setUp(self):
self.app = create_app()
self.app.testing = True
self.test_app = self.app.test_client()
### LOGGED IN ###
def test_is_logged_in_default_session(self):
with self.app.test_request_context('/'):
self.assertFalse('username' in flask.session)
self.assertFalse(is_logged_in())
def test_is_logged_in(self):
with self.app.test_request_context('/'):
flask.session['username'] = 'test'
self.assertTrue('username' in flask.session)
self.assertTrue(is_logged_in())
### ADMIN ###
def test_admin_required(self):
with self.app.test_request_context('/'):
func = Mock()
func.return_value = 5 # a known value to test for
wrapped = admin_required(func)
user_mock = Mock()
user_mock.type = 'admin'
resp = wrapped(user=user_mock)
self.assertEqual(resp, 5)
def test_admin_required_no_user(self):
with self.app.test_request_context('/'):
func = Mock()
wrapped = admin_required(func)
resp = wrapped()
self.assertEqual(resp[1], 401)
def test_admin_required_not_permitted(self):
with self.app.test_request_context('/'):
func = Mock()
wrapped = admin_required(func)
user_mock = Mock()
user_mock.type = 'user'
resp = wrapped(user=user_mock)
self.assertEqual(resp[1], 401)
### SPOTIFY ###
def test_spotify_required(self):
with self.app.test_request_context('/'):
func = Mock()
func.return_value = 5 # a known value to test for
wrapped = spotify_link_required(func)
user_mock = Mock()
user_mock.spotify_linked = True
resp = wrapped(user=user_mock)
self.assertEqual(resp, 5)
def test_spotify_required_no_user(self):
with self.app.test_request_context('/'):
func = Mock()
wrapped = spotify_link_required(func)
resp = wrapped()
self.assertEqual(resp[1], 401)
def test_spotify_required_not_linked(self):
with self.app.test_request_context('/'):
func = Mock()
wrapped = spotify_link_required(func)
user_mock = Mock()
user_mock.spotify_linked = False
resp = wrapped(user=user_mock)
self.assertEqual(resp[1], 401)
### LAST.FM ###
def test_lastfm_required(self):
with self.app.test_request_context('/'):
func = Mock()
func.return_value = 5 # a known value to test for
wrapped = lastfm_username_required(func)
user_mock = Mock()
user_mock.lastfm_username = 'test_username'
resp = wrapped(user=user_mock)
self.assertEqual(resp, 5)
def test_lastfm_required_no_user(self):
with self.app.test_request_context('/'):
func = Mock()
wrapped = lastfm_username_required(func)
resp = wrapped()
self.assertEqual(resp[1], 401)
def test_lastfm_required_zero_length(self):
with self.app.test_request_context('/'):
func = Mock()
wrapped = lastfm_username_required(func)
user_mock = Mock()
user_mock.lastfm_username = ''
resp = wrapped(user=user_mock)
self.assertEqual(resp[1], 401)
### CHECK_DICT ###
def test_check_dict(self):
with self.app.test_request_context('/'):
func = Mock()
func.return_value = 5 # a known value to test for
resp = check_dict(
request_params=["test1", "test2", "test3"],
expected_args=["test1", "test2", "test3"],
func=func,
args=[],
kwargs={}
)
self.assertEqual(resp, 5)
def test_check_dict_missing_required(self):
with self.app.test_request_context('/'):
func = Mock()
func.return_value = 5 # a known value to test for
resp = check_dict(
request_params=["test1", "test2"],
expected_args=["test1", "test2", "test3"],
func=func,
args=[],
kwargs={}
)
self.assertEqual(resp[1], 400)
def test_check_dict_tuples(self):
with self.app.test_request_context('/'):
func = Mock()
func.return_value = 5 # a known value to test for
resp = check_dict(
request_params={"test1": 'hello world', "test2": 10, "test3": True},
expected_args=[("test1", str), ("test2", int), ("test3", bool)],
func=func,
args=[],
kwargs={}
)
self.assertEqual(resp, 5)
def test_check_dict_tuples_wrong_type(self):
with self.app.test_request_context('/'):
func = Mock()
func.return_value = 5 # a known value to test for
resp = check_dict(
request_params={"test1": 'hello world', "test2": "hello world", "test3": True},
expected_args=[("test1", str), ("test2", int), ("test3", bool)],
func=func,
args=[],
kwargs={}
)
self.assertEqual(resp[1], 400)

21
tests/test_model.py Normal file
View File

@ -0,0 +1,21 @@
import unittest
from music.model.user import User
class TestUser(unittest.TestCase):
def test_fetch_all(self):
users = User.collection.fetch()
self.assertIsNotNone(users)
self.assertTrue(len([i for i in users]) > 0)
def test_to_dict(self):
users = [i for i in User.collection.fetch()]
self.assertIsInstance(users[0].to_dict(), dict)
def test_to_dict_filtered_keys(self):
users = [i for i in User.collection.fetch()]
for user in users:
for key in ['password', 'access_token', 'refresh_token', 'token_expiry', 'id', 'key']:
self.assertNotIn(key, user.to_dict())