Adicionar testes unitários

This commit is contained in:
Augusto Gunsch 2021-10-19 00:26:16 -03:00
parent 88f0ff9800
commit 8e8d9237b7
No known key found for this signature in database
GPG Key ID: F7EEFE29825C72DC
10 changed files with 409 additions and 27 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
__pycache__/ __pycache__/
venv/ venv/
pyrightconfig.json pyrightconfig.json
database.db *.db

View File

@ -18,6 +18,12 @@ Inicie o servidor de desenvolvimento:
flask run 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`. Por padrão o servidor tem sua interface em `http://localhost:5000`.
# Desenvolvimento # Desenvolvimento

View File

@ -1,4 +1,3 @@
#!/bin/python3
from flask import Flask from flask import Flask
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow from flask_marshmallow import Marshmallow
@ -10,6 +9,4 @@ db = SQLAlchemy(app)
ma = Marshmallow(app) ma = Marshmallow(app)
Migrate(app, db) Migrate(app, db)
import api.models.trainer
import api.models.pokemon_owned
import api.routes.routes import api.routes.routes

View File

@ -3,15 +3,15 @@ from api.views import trainer, pokemon_owned, helper, errors
from flask import request from flask import request
import asyncio import asyncio
@app.route("/trainer/<int:trainerId>/", methods=["GET"]) @app.route("/trainer/<int:trainerId>", methods=["GET"])
def route_get_trainer(trainerId): def route_get_trainer(trainerId):
return trainer.get_trainer(trainerId) return trainer.get_trainer(trainerId)
@app.route("/trainer/", methods=["GET"]) @app.route("/trainer", methods=["GET"])
def route_get_trainers(): def route_get_trainers():
return trainer.get_trainers() return trainer.get_trainers()
@app.route("/trainer/", methods=["POST"]) @app.route("/trainer", methods=["POST"])
def route_create_trainer(): def route_create_trainer():
return trainer.post_trainer() return trainer.post_trainer()

360
api/tests.py Executable file
View File

@ -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()

View File

@ -21,4 +21,4 @@ def ForbiddenError(message):
return error(5, "ForbiddenError", message, http_code=403) return error(5, "ForbiddenError", message, http_code=403)
def FetchError(message): def FetchError(message):
return error(6, "FetchError", message, http_code=500) return error(6, "FetchError", message, http_code=404)

View File

@ -12,16 +12,17 @@ class HTTPError(Exception):
self.message = message self.message = message
class NotFound(Exception): class NotFound(Exception):
pass def __init__(self, message):
self.message = message
def get_or_not_found(callback): def get_or_not_found(callback):
try: try:
resource = callback() resource = callback()
if resource is None: if resource is None:
raise NotFound() raise NotFound("Resource not found")
return resource return resource
except: except:
raise NotFound() raise NotFound("Resource not found")
def get_trainer_fail(id): def get_trainer_fail(id):
return get_or_not_found(lambda : Trainer.query.get(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 f(trainer, *args, **kwargs)
return decorated 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 # seguintes funções puxam informações da pokeapi
def set_pokemon_data(pokemon): def set_pokemon_data(pokemon):
try: try:
response = requests.get("https://pokeapi.co/api/v2/pokemon/{}".format(pokemon.pokemon_id)) response = requests.get("https://pokeapi.co/api/v2/pokemon/{}".format(pokemon.pokemon_id))
if response.status_code != 200: 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) pokemon.pokemon_data = json.loads(response.text)
except: 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): async def async_set_pokemon_data(session, pokemon):
try: try:
response = await session.get("https://pokeapi.co/api/v2/pokemon/{}".format(pokemon.pokemon_id)) response = await session.get("https://pokeapi.co/api/v2/pokemon/{}".format(pokemon.pokemon_id))
if response.status != 200: 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()) pokemon.pokemon_data = json.loads(await response.text())
except: except:
raise HTTPError("Could not fetch pokemon with id {}".format(pokemon.pokemon_id)) cant_fetch_error(pokemon)

View File

@ -4,7 +4,7 @@ from .parse_args import parse_limit, parse_offset, ParsingException, parse_json_
from .errors import ParsingError, FetchError, ConflictingResources from .errors import ParsingError, FetchError, ConflictingResources
from .helper import * from .helper import *
from aiohttp import ClientSession from aiohttp import ClientSession
from asyncio import create_task, gather import asyncio
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
def post_pokemon_owned(trainer_id): def post_pokemon_owned(trainer_id):
@ -30,7 +30,7 @@ def post_pokemon_owned(trainer_id):
db.session.add(pokemon) db.session.add(pokemon)
db.session.commit() db.session.commit()
except HTTPError as e: except NotFound as e:
return FetchError(e.message) return FetchError(e.message)
except IntegrityError: except IntegrityError:
return ConflictingResources("Trainer already has another pokemon with the same name") 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: async with ClientSession() as session:
tasks = [] tasks = []
for pokemon in pokemons: 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) tasks.append(task)
try: try:
await gather(*tasks) await asyncio.gather(*tasks)
except HTTPError as e: except NotFound as e:
for task in tasks:
task.cancel()
return FetchError(e.message) 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) return pokemon_owned_schemas.dumps(pokemons)
def get_pokemon_owned(trainer_id, pokemon_id): def get_pokemon_owned(trainer_id, pokemon_id):
@ -73,6 +81,8 @@ def get_pokemon_owned(trainer_id, pokemon_id):
return "", 404 return "", 404
except HTTPError as e: except HTTPError as e:
return FetchError(e.message) return FetchError(e.message)
except NotFound as e:
return ParsingError(e.message)
def delete_pokemon_owned(trainer, pokemon_id): def delete_pokemon_owned(trainer, pokemon_id):
try: try:

View File

@ -11,7 +11,10 @@ import jwt
def get_trainer(id): def get_trainer(id):
try: 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: except:
return ("", 404) return ("", 404)

View File

@ -1,9 +1,10 @@
Flask_SQLAlchemy==2.5.1
requests==2.26.0
aiohttp==3.7.4.post0
Werkzeug==2.0.2 Werkzeug==2.0.2
Flask==2.0.2 Flask_SQLAlchemy==2.5.1
SQLAlchemy==1.4.25
Flask_Migrate==3.1.0
flask_marshmallow==0.14.0 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 PyJWT==2.3.0