Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Close #30 Persist and test API ns for projects
  • Loading branch information
EliuX committed Mar 24, 2020
commit e1eacf7f55d20dacae25a519b7779b718c271083
8 changes: 4 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ def client(app: Flask) -> FlaskClient:
@pytest.fixture(scope="module")
def sql_repository():
from .resources import PersonSQLModel
from time_tracker_api.database import seeder
from time_tracker_api.sql_repository import db

seeder.fresh()
db.metadata.create_all(bind=db.engine, tables=[PersonSQLModel.__table__])
print("Test models created!")

from time_tracker_api.sql_repository import SQLRepository
yield SQLRepository(PersonSQLModel)

db.drop_all()
print("Models for test removed!")
db.metadata.drop_all(bind=db.engine, tables=[PersonSQLModel.__table__])
print("Test models removed!")
191 changes: 188 additions & 3 deletions tests/projects/projects_namespace_test.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,201 @@
from faker import Faker
from flask import json
from flask.testing import FlaskClient
from pytest_mock import MockFixture

from time_tracker_api.projects.projects_model import PROJECT_TYPE

def test_list_all_elements(client: FlaskClient, mocker: MockFixture):
fake = Faker()

valid_project_data = {
"name": fake.company(),
"description": fake.paragraph(),
"type": fake.word(PROJECT_TYPE.valid_type_values()),
}
fake_project = ({
"id": fake.random_int(1, 9999)
}).update(valid_project_data)


def test_create_project_should_succeed_with_valid_request(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.projects.projects_namespace import project_dao
repository_create_mock = mocker.patch.object(project_dao.repository,
'create',
return_value=fake_project)

response = client.post("/projects", json=valid_project_data, follow_redirects=True)

assert 201 == response.status_code
repository_create_mock.assert_called_once_with(valid_project_data)


def test_create_project_should_reject_bad_request(client: FlaskClient, mocker: MockFixture):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can use the name test_create_project_should_fail_with_invalid_request for consistency with the previous request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather to leave bad request because that is the verbose name of the HTTP status code 400.

from time_tracker_api.projects.projects_namespace import project_dao
repository_find_all_mock = mocker.patch.object(project_dao.repository, 'find_all', return_value=[])
invalid_project_data = valid_project_data.copy().update({
"type": 'anything',
})
repository_create_mock = mocker.patch.object(project_dao.repository,
'create',
return_value=fake_project)

response = client.post("/projects", json=invalid_project_data, follow_redirects=True)

assert 400 == response.status_code
repository_create_mock.assert_not_called()


def test_list_all_projects(client: FlaskClient, mocker: MockFixture):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about test_get_all_projects or test_get_projects?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description in the comments says list. As long as it makes sense and it is not ambiguous we are Ok.

from time_tracker_api.projects.projects_namespace import project_dao
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice, you are calling from time_tracker_api.projects.projects_namespace import project_dao in all the tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if I do it before the project_dao would be broken because it has to be constructed after the app instance is ready. This is a necessary evil.

repository_find_all_mock = mocker.patch.object(project_dao.repository,
'find_all',
return_value=[])

response = client.get("/projects", follow_redirects=True)

assert 200 == response.status_code

json_data = json.loads(response.data)
assert [] == json_data
repository_find_all_mock.assert_called_once()


def test_get_project_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.projects.projects_namespace import project_dao

valid_id = fake.random_int(1, 9999)

repository_find_mock = mocker.patch.object(project_dao.repository,
'find',
return_value=fake_project)

response = client.get("/projects/%s" % valid_id, follow_redirects=True)

assert 200 == response.status_code
fake_project == json.loads(response.data)
repository_find_mock.assert_called_once_with(str(valid_id))


def test_get_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.projects.projects_namespace import project_dao
from werkzeug.exceptions import NotFound

invalid_id = fake.random_int(1, 9999)

repository_find_mock = mocker.patch.object(project_dao.repository,
'find',
side_effect=NotFound)

response = client.get("/projects/%s" % invalid_id, follow_redirects=True)

assert 404 == response.status_code
repository_find_mock.assert_called_once_with(str(invalid_id))


def test_get_project_should_return_422_for_invalid_id_format(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.projects.projects_namespace import project_dao
from werkzeug.exceptions import UnprocessableEntity

invalid_id = fake.company()

repository_find_mock = mocker.patch.object(project_dao.repository,
'find',
side_effect=UnprocessableEntity)

response = client.get("/projects/%s" % invalid_id, follow_redirects=True)

assert 422 == response.status_code
repository_find_mock.assert_called_once_with(str(invalid_id))


def update_project_should_succeed_with_valid_data(client: FlaskClient, mocker: MockFixture):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are missing the test_ prefix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch

from time_tracker_api.projects.projects_namespace import project_dao

repository_update_mock = mocker.patch.object(project_dao.repository,
'update',
return_value=fake_project)

valid_id = fake.random_int(1, 9999)
response = client.put("/projects/%s" % valid_id, json=valid_project_data, follow_redirects=True)

assert 200 == response.status_code
fake_project == json.loads(response.data)
repository_update_mock.assert_called_once_with(valid_id, valid_project_data)


def test_update_project_should_reject_bad_request(client: FlaskClient, mocker: MockFixture):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can use the name test_update_project_should_fail_with_invalid_request as before.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bad request is better

from time_tracker_api.projects.projects_namespace import project_dao
invalid_project_data = valid_project_data.copy().update({
"type": 'anything',
})
repository_update_mock = mocker.patch.object(project_dao.repository,
'update',
return_value=fake_project)

valid_id = fake.random_int(1, 9999)
response = client.put("/projects/%s" % valid_id, json=invalid_project_data, follow_redirects=True)

assert 400 == response.status_code
repository_update_mock.assert_not_called()


def test_update_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.projects.projects_namespace import project_dao
from werkzeug.exceptions import NotFound

invalid_id = fake.random_int(1, 9999)

repository_update_mock = mocker.patch.object(project_dao.repository,
'update',
side_effect=NotFound)

response = client.put("/projects/%s" % invalid_id, json=valid_project_data, follow_redirects=True)

assert 404 == response.status_code
repository_update_mock.assert_called_once_with(str(invalid_id), valid_project_data)


def test_delete_project_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.projects.projects_namespace import project_dao

valid_id = fake.random_int(1, 9999)

repository_remove_mock = mocker.patch.object(project_dao.repository,
'remove',
return_value=None)

response = client.delete("/projects/%s" % valid_id, follow_redirects=True)

assert 204 == response.status_code
assert b'' == response.data
repository_remove_mock.assert_called_once_with(str(valid_id))


def test_delete_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.projects.projects_namespace import project_dao
from werkzeug.exceptions import NotFound

invalid_id = fake.random_int(1, 9999)

repository_remove_mock = mocker.patch.object(project_dao.repository,
'remove',
side_effect=NotFound)

response = client.delete("/projects/%s" % invalid_id, follow_redirects=True)

assert 404 == response.status_code
repository_remove_mock.assert_called_once_with(str(invalid_id))


def test_delete_project_should_return_422_for_invalid_id_format(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.projects.projects_namespace import project_dao
from werkzeug.exceptions import UnprocessableEntity

invalid_id = fake.company()

repository_remove_mock = mocker.patch.object(project_dao.repository,
'remove',
side_effect=UnprocessableEntity)

response = client.delete("/projects/%s" % invalid_id, follow_redirects=True)

assert 422 == response.status_code
repository_remove_mock.assert_called_once_with(str(invalid_id))
7 changes: 5 additions & 2 deletions time_tracker_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import os

from flask import Flask
Expand Down Expand Up @@ -34,16 +35,18 @@ def init_app_config(app: Flask, config_path: str, config_data: dict = None):


def init_app(app: Flask):
from .database import init_app as init_database
from time_tracker_api.database import init_app as init_database
init_database(app)

from .api import api
from time_tracker_api.api import api
api.init_app(app)

if app.config.get('DEBUG'):
app.logger.setLevel(logging.INFO)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not be app.logger.setLevel(logging.DEBUG)?

Copy link
Contributor Author

@EliuX EliuX Mar 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO logging.INFO is detailed enough.

add_debug_toolbar(app)



def add_debug_toolbar(app):
app.config['DEBUG_TB_PANELS'] = (
'flask_debugtoolbar.panels.versions.VersionDebugPanel',
Expand Down
8 changes: 0 additions & 8 deletions time_tracker_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,6 @@
description='Date of update',
example=faker.iso8601(end_datetime=None),
),
# TODO Activate it when the tenants model is implemented
# 'tenant_id': fields.String(
# readOnly=True,
# title='Tenant',
# max_length=64,
# description='The tenant this belongs to',
# example=faker.random_int(1, 9999),
# ),
'created_by': fields.String(
readOnly=True,
title='Creator',
Expand Down
1 change: 1 addition & 0 deletions time_tracker_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Config:
SECRET_KEY = generate_dev_secret_key()
DATABASE_URI = os.environ.get('DATABASE_URI')
PROPAGATE_EXCEPTIONS = True
RESTPLUS_VALIDATE = True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good.



class DevelopConfig(Config):
Expand Down
2 changes: 1 addition & 1 deletion time_tracker_api/projects/projects_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from flask import Flask

from time_tracker_api.database import CRUDDao
from time_tracker_api.sql_repository import SQLCRUDDao, AuditedSQLModel, SQLModel


class PROJECT_TYPE(enum.Enum):
Expand All @@ -21,6 +20,7 @@ class ProjectDao(CRUDDao):

def create_dao(app: Flask) -> ProjectDao:
from time_tracker_api.sql_repository import db
from time_tracker_api.sql_repository import SQLCRUDDao, AuditedSQLModel, SQLModel

class ProjectSQLModel(db.Model, SQLModel, AuditedSQLModel):
__tablename__ = 'projects'
Expand Down
13 changes: 7 additions & 6 deletions time_tracker_api/projects/projects_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from time_tracker_api import flask_app
from time_tracker_api.api import audit_fields
from .projects_model import PROJECT_TYPE, create_dao
from time_tracker_api.projects.projects_model import PROJECT_TYPE, create_dao

faker = Faker()

Expand Down Expand Up @@ -65,21 +65,19 @@ class Projects(Resource):
@ns.doc('list_projects')
@ns.marshal_list_with(project, code=200)
def get(self):
"""List all projects"""
return project_dao.get_all(), 200

@ns.doc('create_project')
@ns.response(409, 'This project already exists')
@ns.response(422, 'The data has an invalid format')
@ns.response(400, 'Bad request')
@ns.expect(project_input)
@ns.marshal_with(project, code=201)
def post(self):
"""Create a project"""
return project_dao.create(ns.payload), 201


# TODO : fix, this parser is for a field that is not being used.
project_update_parser = ns.parser()


@ns.route('/<string:id>')
@ns.response(404, 'Project not found')
@ns.param('id', 'The project identifier')
Expand All @@ -88,6 +86,7 @@ class Project(Resource):
@ns.response(422, 'The id has an invalid format')
@ns.marshal_with(project)
def get(self, id):
"""Get a project"""
return project_dao.get(id)

@ns.doc('update_project')
Expand All @@ -96,11 +95,13 @@ def get(self, id):
@ns.expect(project_input)
@ns.marshal_with(project)
def put(self, id):
"""Update a project"""
return project_dao.update(id, ns.payload)

@ns.doc('delete_project')
@ns.response(204, 'Project deleted successfully')
@ns.response(422, 'The id has an invalid format')
def delete(self, id):
"""Delete a project"""
project_dao.delete(id)
return None, 204
2 changes: 1 addition & 1 deletion time_tracker_api/sql_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from time_tracker_api.database import CRUDDao, Seeder, DatabaseModel, convert_result_to_dto
from time_tracker_api.security import current_user_id

db = None
db: SQLAlchemy = None
SQLModel = None
AuditedSQLModel = None

Expand Down