From 8e8d9237b7c42bf05dc3ed4b5cdb20fa88173e6d Mon Sep 17 00:00:00 2001 From: Augusto Gunsch Date: Tue, 19 Oct 2021 00:26:16 -0300 Subject: [PATCH] =?UTF-8?q?Adicionar=20testes=20unit=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- README.md | 6 + api/app.py | 3 - api/routes/routes.py | 6 +- api/tests.py | 360 +++++++++++++++++++++++++++++++++++++ api/views/errors.py | 2 +- api/views/helper.py | 19 +- api/views/pokemon_owned.py | 20 ++- api/views/trainer.py | 5 +- requirements.txt | 13 +- 10 files changed, 409 insertions(+), 27 deletions(-) create mode 100755 api/tests.py diff --git a/.gitignore b/.gitignore index 4c81dc6..2026a7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ __pycache__/ venv/ pyrightconfig.json -database.db +*.db diff --git a/README.md b/README.md index 7e80cc6..9f0007f 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,12 @@ Inicie o servidor de desenvolvimento: flask run ``` +## Rodando Testes +No diretório **raíz**: +``` +python3 -m api.tests +``` + Por padrão o servidor tem sua interface em `http://localhost:5000`. # Desenvolvimento diff --git a/api/app.py b/api/app.py index b8fb2e2..9934a5e 100755 --- a/api/app.py +++ b/api/app.py @@ -1,4 +1,3 @@ -#!/bin/python3 from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_marshmallow import Marshmallow @@ -10,6 +9,4 @@ db = SQLAlchemy(app) ma = Marshmallow(app) Migrate(app, db) -import api.models.trainer -import api.models.pokemon_owned import api.routes.routes diff --git a/api/routes/routes.py b/api/routes/routes.py index d1a35eb..9bbc2b2 100644 --- a/api/routes/routes.py +++ b/api/routes/routes.py @@ -3,15 +3,15 @@ from api.views import trainer, pokemon_owned, helper, errors from flask import request import asyncio -@app.route("/trainer//", methods=["GET"]) +@app.route("/trainer/", methods=["GET"]) def route_get_trainer(trainerId): return trainer.get_trainer(trainerId) -@app.route("/trainer/", methods=["GET"]) +@app.route("/trainer", methods=["GET"]) def route_get_trainers(): return trainer.get_trainers() -@app.route("/trainer/", methods=["POST"]) +@app.route("/trainer", methods=["POST"]) def route_create_trainer(): return trainer.post_trainer() diff --git a/api/tests.py b/api/tests.py new file mode 100755 index 0000000..267ea79 --- /dev/null +++ b/api/tests.py @@ -0,0 +1,360 @@ +#!/bin/python3 +from api.app import app, db +from api.models.trainer import Trainer +from api.models.pokemon_owned import PokemonOwned +from flask_testing import TestCase +import unittest + +class MainTestCase(TestCase): + + def create_app(self): + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///testing.db" + db.drop_all() + db.create_all() + trainers = ( + Trainer( + nickname="jose", + first_name="José", + last_name="da Silva", + email="josedasilva.2021@gmail.com", + password="1234", + team="Team Valor" + ), + Trainer( + nickname="joao", + first_name="João", + last_name="Oliveira", + email="joaooliveira@hotmail.com", + password="senha", + team="Team Instinct" + ), + Trainer( + nickname="ricardo", + first_name="Ricardo", + last_name="Teixeira", + email="ricardo.teixeira@gmail.com", + password="ricardo", + team="Team Mystic" + ), + ) + pokemons = ( + PokemonOwned( + name="Fluffy", + level=1, + pokemon_id=12, + trainer_id=1 + ), + PokemonOwned( + name="Dinossauro", + level=1, + pokemon_id=2, + trainer_id=1 + ), + PokemonOwned( + name="Outro", + level=1, + pokemon_id=4, + trainer_id=1 + ), + ) + db.session.add_all(trainers) + db.session.add_all(pokemons) + db.session.commit() + self.client = app.test_client() + return app + + def test_post_trainer(self): + trainer = { + "nickname": "rodrigro", + "first_name": "Ricardo", + "last_name": "Lopes", + "email": "rlopes@outlook.com", + "password": "dummy", + "team": "Team Mystic" + } + response = self.client.post("/trainer", json=trainer, follow_redirects=True) + self.assert_status(response, 201) + self.assertEqual(trainer["email"], response.get_json()["email"]) + + def test_post_trainer_duplicate(self): + trainer = { + "nickname": "julio", + "first_name": "Julio", + "last_name": "Sobreiro", + "email": "julho.sob@yahoo.com", + "password": "0987", + "team": "Team Mystic" + } + self.client.post("/trainer", json=trainer, follow_redirects=True) + response = self.client.post("/trainer", json=trainer, follow_redirects=True) + self.assert_status(response, 409) + + def test_post_trainer_invalid_team(self): + trainer = { + "nickname": "cesar", + "first_name": "Cesar", + "last_name": "Pereira", + "email": "cesereira@gmail.com", + "password": "04031994", + "team": "Team Inventado" + } + response = self.client.post("/trainer", json=trainer, follow_redirects=True) + self.assert_400(response) + + def test_post_trainer_missing_fields(self): + trainer = {} + response = self.client.post("/trainer", json=trainer, follow_redirects=True) + self.assert_400(response) + + def test_authenticate(self): + login = { + "email": "josedasilva.2021@gmail.com", + "password": "1234", + } + response = self.client.post("/trainer/authenticate", json=login, follow_redirects=True) + self.assert_200(response) + self.assertIn(b"id", response.data) + self.assertIn(b"token", response.data) + + def test_authenticate_wrong(self): + login = { + "email": "josedasilva.2021@gmail.com", + "password": "wrong_password", + } + response = self.client.post("/trainer/authenticate", json=login, follow_redirects=True) + self.assert_401(response) + + def test_authenticate_not_found(self): + login = { + "email": "notrainer@withemail.com", + "password": "dummy_password", + } + response = self.client.post("/trainer/authenticate", json=login, follow_redirects=True) + self.assert_401(response) + + def test_authenticate_no_login(self): + login = {} + response = self.client.post("/trainer/authenticate", json=login, follow_redirects=True) + self.assert_401(response) + + def test_get_trainers(self): + response = self.client.get("/trainer", follow_redirects=True) + self.assert_200(response) + self.assertIn(b"jose", response.data) + self.assertIn(b"joao", response.data) + self.assertIn(b"ricardo", response.data) + + def test_get_trainer_by_nickname(self): + response = self.client.get("/trainer?nickname=jose", follow_redirects=True) + self.assert_200(response) + self.assertIn(b"jose", response.data) + + def test_get_trainer_by_nickname_not_found(self): + response = self.client.get("/trainer?nickname=somerandomnickname", follow_redirects=True) + self.assert_200(response) + self.assertEqual(response.data, b"[]") + + def test_get_trainer_by_nickname_contains(self): + response = self.client.get("/trainer?nickname_contains=jo", follow_redirects=True) + self.assert_200(response) + self.assertIn(b"jose", response.data) + self.assertIn(b"joao", response.data) + + def test_get_trainer_by_nickname_contains_limit(self): + response = self.client.get("/trainer?nickname_contains=jo&limit=1", follow_redirects=True) + self.assert_200(response) + self.assertIn(b"jose", response.data) + self.assertNotIn(b"joao", response.data) + + def test_get_trainer_by_nickname_contains_offset(self): + response = self.client.get("/trainer?nickname_contains=jo&offset=1", follow_redirects=True) + self.assert_200(response) + self.assertNotIn(b"jose", response.data) + self.assertIn(b"joao", response.data) + + def test_get_trainer_by_id(self): + response = self.client.get("/trainer/1", follow_redirects=True) + self.assert_200(response) + self.assertIn(b"jose", response.data) + self.assertNotIn(b"joao", response.data) + + def test_get_trainer_by_id_not_found(self): + response = self.client.get("/trainer/1000", follow_redirects=True) + self.assertEqual(response.status_code, 404) + + def test_get_pokemons(self): + response = self.client.get("/trainer/1/pokemon", follow_redirects=True) + self.assert_200(response) + self.assertIn(b"Fluffy", response.data) + self.assertIn(b"Dinossauro", response.data) + self.assertIn(b"Outro", response.data) + self.assertIn(b"pokemon_data", response.data) + + def test_get_pokemons_trainer_not_found(self): + response = self.client.get("/trainer/1000/pokemon", follow_redirects=True) + self.assertEqual(response.status_code, 404) + + def test_get_pokemons_limit(self): + response = self.client.get("/trainer/1/pokemon?limit=1", follow_redirects=True) + self.assert_200(response) + self.assertIn(b"Fluffy", response.data) + self.assertNotIn(b"Dinossauro", response.data) + self.assertNotIn(b"Outro", response.data) + + def test_get_pokemons_offset(self): + response = self.client.get("/trainer/1/pokemon?offset=1", follow_redirects=True) + self.assert_200(response) + self.assertNotIn(b"Fluffy", response.data) + self.assertIn(b"Dinossauro", response.data) + self.assertIn(b"Outro", response.data) + + def test_get_pokemon_by_id(self): + response = self.client.get("/trainer/1/pokemon/2", follow_redirects=True) + self.assert_200(response) + self.assertNotIn(b"Fluffy", response.data) + self.assertIn(b"Dinossauro", response.data) + self.assertNotIn(b"Outro", response.data) + self.assertIn(b"pokemon_data", response.data) + + def test_get_pokemon_by_id_not_found(self): + response = self.client.get("/trainer/1/pokemon/1000", follow_redirects=True) + self.assertEqual(response.status_code, 404) + + def test_post_pokemon_no_auth(self): + data = { + "name": "Dummy", + "level": 9, + "pokemon_id": 12 + } + response = self.client.post("/trainer/2/pokemon", json=data, follow_redirects=True) + self.assert_401(response) + + def test_post_pokemon(self): + login = { + "email": "joaooliveira@hotmail.com", + "password": "senha", + } + auth = self.client.post("/trainer/authenticate", json=login, follow_redirects=True) + self.assert_200(auth) + token = auth.get_json()["token"] + data = { + "name": "Dummy", + "level": 2, + "pokemon_id": 12 + } + response = self.client.post("/trainer/2/pokemon", json=data, headers={"Authorization":token}, follow_redirects=True) + self.assert_status(response, 201) + self.assertIn(b"Dummy", response.data) + self.assertIn(b"pokemon_data", response.data) + + def test_post_pokemon_trainer_not_found(self): + login = { + "email": "joaooliveira@hotmail.com", + "password": "senha", + } + auth = self.client.post("/trainer/authenticate", json=login, follow_redirects=True) + self.assert_200(auth) + token = auth.get_json()["token"] + data = { + "name": "Dummy", + "level": 2, + "pokemon_id": 12 + } + response = self.client.post("/trainer/200/pokemon", json=data, headers={"Authorization":token}, follow_redirects=True) + self.assert_403(response) + + # adicionando pokemon pra outro trainer + def test_post_pokemon_forbidden(self): + login = { + "email": "joaooliveira@hotmail.com", + "password": "senha", + } + auth = self.client.post("/trainer/authenticate", json=login, follow_redirects=True) + self.assert_200(auth) + token = auth.get_json()["token"] + data = { + "name": "Dummy", + "level": 2, + "pokemon_id": 12 + } + response = self.client.post("/trainer/1/pokemon", json=data, headers={"Authorization":token}, follow_redirects=True) + self.assert_403(response) + + def test_post_pokemon_no_species(self): + login = { + "email": "joaooliveira@hotmail.com", + "password": "senha", + } + auth = self.client.post("/trainer/authenticate", json=login, follow_redirects=True) + self.assert_200(auth) + token = auth.get_json()["token"] + data = { + "name": "Dumb", + "level": 2, + "pokemon_id": 12000 + } + response = self.client.post("/trainer/2/pokemon", json=data, headers={"Authorization":token}, follow_redirects=True) + self.assert_404(response) + + def test_delete_pokemon_trainer_not_found(self): + login = { + "email": "joaooliveira@hotmail.com", + "password": "senha", + } + auth = self.client.post("/trainer/authenticate", json=login, follow_redirects=True) + self.assert_200(auth) + token = auth.get_json()["token"] + response = self.client.delete("/trainer/200/pokemon/1", headers={"Authorization":token}, follow_redirects=True) + self.assert_403(response) + + def test_delete_pokemon_no_auth(self): + login = { + "email": "joaooliveira@hotmail.com", + "password": "senha", + } + auth = self.client.post("/trainer/authenticate", json=login, follow_redirects=True) + self.assert_200(auth) + token = auth.get_json()["token"] + data = { + "name": "Dummier", + "level": 2, + "pokemon_id": 12 + } + response = self.client.post("/trainer/2/pokemon", json=data, headers={"Authorization":token}, follow_redirects=True) + self.assert_status(response, 201) + response = self.client.delete("/trainer/2/pokemon/{}".format(response.get_json()["id"]), follow_redirects=True) + self.assert_401(response) + + def test_delete_pokemon_not_found(self): + login = { + "email": "joaooliveira@hotmail.com", + "password": "senha", + } + auth = self.client.post("/trainer/authenticate", json=login, follow_redirects=True) + self.assert_200(auth) + token = auth.get_json()["token"] + response = self.client.delete("/trainer/2/pokemon/1000", headers={"Authorization":token}, follow_redirects=True) + self.assert_404(response) + + def test_delete_pokemon(self): + login = { + "email": "joaooliveira@hotmail.com", + "password": "senha", + } + auth = self.client.post("/trainer/authenticate", json=login, follow_redirects=True) + self.assert_200(auth) + token = auth.get_json()["token"] + data = { + "name": "Dummier", + "level": 2, + "pokemon_id": 12 + } + response = self.client.post("/trainer/2/pokemon", json=data, headers={"Authorization":token}, follow_redirects=True) + self.assert_status(response, 201) + response = self.client.delete("/trainer/2/pokemon/{}".format(response.get_json()["id"]), headers={"Authorization":token}, follow_redirects=True) + self.assert_200(response) + + + +if __name__ == "__main__": + unittest.main() diff --git a/api/views/errors.py b/api/views/errors.py index 7934cbf..dd52afc 100644 --- a/api/views/errors.py +++ b/api/views/errors.py @@ -21,4 +21,4 @@ def ForbiddenError(message): return error(5, "ForbiddenError", message, http_code=403) def FetchError(message): - return error(6, "FetchError", message, http_code=500) + return error(6, "FetchError", message, http_code=404) diff --git a/api/views/helper.py b/api/views/helper.py index ac47f68..843ac3f 100644 --- a/api/views/helper.py +++ b/api/views/helper.py @@ -12,16 +12,17 @@ class HTTPError(Exception): self.message = message class NotFound(Exception): - pass + def __init__(self, message): + self.message = message def get_or_not_found(callback): try: resource = callback() if resource is None: - raise NotFound() + raise NotFound("Resource not found") return resource except: - raise NotFound() + raise NotFound("Resource not found") def get_trainer_fail(id): return get_or_not_found(lambda : Trainer.query.get(id)) @@ -50,21 +51,25 @@ def token_required(f): return f(trainer, *args, **kwargs) return decorated +# helpers internos +def cant_fetch_error(pokemon): + raise NotFound("Could not fetch data for pokemon with id {}".format(pokemon.pokemon_id)) + # seguintes funções puxam informações da pokeapi def set_pokemon_data(pokemon): try: response = requests.get("https://pokeapi.co/api/v2/pokemon/{}".format(pokemon.pokemon_id)) if response.status_code != 200: - raise HTTPError("Could not fetch pokemon with id {}".format(pokemon.pokemon_id)) + cant_fetch_error(pokemon) pokemon.pokemon_data = json.loads(response.text) except: - raise HTTPError("Could not fetch pokemon with id {}".format(pokemon.pokemon_id)) + cant_fetch_error(pokemon) async def async_set_pokemon_data(session, pokemon): try: response = await session.get("https://pokeapi.co/api/v2/pokemon/{}".format(pokemon.pokemon_id)) if response.status != 200: - raise HTTPError("Could not fetch pokemon with id {}".format(pokemon.pokemon_id)) + cant_fetch_error(pokemon) pokemon.pokemon_data = json.loads(await response.text()) except: - raise HTTPError("Could not fetch pokemon with id {}".format(pokemon.pokemon_id)) + cant_fetch_error(pokemon) diff --git a/api/views/pokemon_owned.py b/api/views/pokemon_owned.py index 40d26cb..6481a16 100644 --- a/api/views/pokemon_owned.py +++ b/api/views/pokemon_owned.py @@ -4,7 +4,7 @@ from .parse_args import parse_limit, parse_offset, ParsingException, parse_json_ from .errors import ParsingError, FetchError, ConflictingResources from .helper import * from aiohttp import ClientSession -from asyncio import create_task, gather +import asyncio from sqlalchemy.exc import IntegrityError def post_pokemon_owned(trainer_id): @@ -30,7 +30,7 @@ def post_pokemon_owned(trainer_id): db.session.add(pokemon) db.session.commit() - except HTTPError as e: + except NotFound as e: return FetchError(e.message) except IntegrityError: return ConflictingResources("Trainer already has another pokemon with the same name") @@ -54,13 +54,21 @@ async def get_pokemons_owned(trainer_id): async with ClientSession() as session: tasks = [] for pokemon in pokemons: - task = create_task(async_set_pokemon_data(session, pokemon)) + task = asyncio.create_task(async_set_pokemon_data(session, pokemon)) tasks.append(task) try: - await gather(*tasks) - except HTTPError as e: + await asyncio.gather(*tasks) + except NotFound as e: + for task in tasks: + task.cancel() return FetchError(e.message) + # workaround pra bug do aiohttp, que pode gerar avisos de conexões abertas + # ver documentação: https://docs.aiohttp.org/en/stable/client_advanced.html#graceful-shutdown + # será arrumado na versão 4.0.0, que ainda não saiu + # issue no github: https://github.com/aio-libs/aiohttp/issues/1925 + await asyncio.sleep(0.250) + return pokemon_owned_schemas.dumps(pokemons) def get_pokemon_owned(trainer_id, pokemon_id): @@ -73,6 +81,8 @@ def get_pokemon_owned(trainer_id, pokemon_id): return "", 404 except HTTPError as e: return FetchError(e.message) + except NotFound as e: + return ParsingError(e.message) def delete_pokemon_owned(trainer, pokemon_id): try: diff --git a/api/views/trainer.py b/api/views/trainer.py index eac1cc4..376c9a7 100644 --- a/api/views/trainer.py +++ b/api/views/trainer.py @@ -11,7 +11,10 @@ import jwt def get_trainer(id): try: - return trainer_schema.dump(Trainer.query.get(id)) + trainer = Trainer.query.get(id) + if trainer is None: + return ("", 404) + return trainer_schema.dump(trainer) except: return ("", 404) diff --git a/requirements.txt b/requirements.txt index 2d4f809..813ed42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ -Flask_SQLAlchemy==2.5.1 -requests==2.26.0 -aiohttp==3.7.4.post0 Werkzeug==2.0.2 -Flask==2.0.2 -SQLAlchemy==1.4.25 -Flask_Migrate==3.1.0 +Flask_SQLAlchemy==2.5.1 flask_marshmallow==0.14.0 +Flask_Migrate==3.1.0 +Flask==2.0.2 +requests==2.26.0 +Flask_Testing==0.8.1 +SQLAlchemy==1.4.25 +aiohttp==3.7.4.post0 PyJWT==2.3.0