Skip to content

Commit e1eacf7

Browse files
author
EliuX
committed
Close #30 Persist and test API ns for projects
1 parent d5a4e5c commit e1eacf7

File tree

8 files changed

+207
-25
lines changed

8 files changed

+207
-25
lines changed

tests/conftest.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ def client(app: Flask) -> FlaskClient:
2222
@pytest.fixture(scope="module")
2323
def sql_repository():
2424
from .resources import PersonSQLModel
25-
from time_tracker_api.database import seeder
2625
from time_tracker_api.sql_repository import db
2726

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

3030
from time_tracker_api.sql_repository import SQLRepository
3131
yield SQLRepository(PersonSQLModel)
3232

33-
db.drop_all()
34-
print("Models for test removed!")
33+
db.metadata.drop_all(bind=db.engine, tables=[PersonSQLModel.__table__])
34+
print("Test models removed!")
Lines changed: 188 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,201 @@
1+
from faker import Faker
12
from flask import json
23
from flask.testing import FlaskClient
34
from pytest_mock import MockFixture
45

6+
from time_tracker_api.projects.projects_model import PROJECT_TYPE
57

6-
def test_list_all_elements(client: FlaskClient, mocker: MockFixture):
8+
fake = Faker()
9+
10+
valid_project_data = {
11+
"name": fake.company(),
12+
"description": fake.paragraph(),
13+
"type": fake.word(PROJECT_TYPE.valid_type_values()),
14+
}
15+
fake_project = ({
16+
"id": fake.random_int(1, 9999)
17+
}).update(valid_project_data)
18+
19+
20+
def test_create_project_should_succeed_with_valid_request(client: FlaskClient, mocker: MockFixture):
21+
from time_tracker_api.projects.projects_namespace import project_dao
22+
repository_create_mock = mocker.patch.object(project_dao.repository,
23+
'create',
24+
return_value=fake_project)
25+
26+
response = client.post("/projects", json=valid_project_data, follow_redirects=True)
27+
28+
assert 201 == response.status_code
29+
repository_create_mock.assert_called_once_with(valid_project_data)
30+
31+
32+
def test_create_project_should_reject_bad_request(client: FlaskClient, mocker: MockFixture):
733
from time_tracker_api.projects.projects_namespace import project_dao
8-
repository_find_all_mock = mocker.patch.object(project_dao.repository, 'find_all', return_value=[])
34+
invalid_project_data = valid_project_data.copy().update({
35+
"type": 'anything',
36+
})
37+
repository_create_mock = mocker.patch.object(project_dao.repository,
38+
'create',
39+
return_value=fake_project)
40+
41+
response = client.post("/projects", json=invalid_project_data, follow_redirects=True)
42+
43+
assert 400 == response.status_code
44+
repository_create_mock.assert_not_called()
45+
46+
47+
def test_list_all_projects(client: FlaskClient, mocker: MockFixture):
48+
from time_tracker_api.projects.projects_namespace import project_dao
49+
repository_find_all_mock = mocker.patch.object(project_dao.repository,
50+
'find_all',
51+
return_value=[])
952

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

1255
assert 200 == response.status_code
13-
1456
json_data = json.loads(response.data)
1557
assert [] == json_data
1658
repository_find_all_mock.assert_called_once()
59+
60+
61+
def test_get_project_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture):
62+
from time_tracker_api.projects.projects_namespace import project_dao
63+
64+
valid_id = fake.random_int(1, 9999)
65+
66+
repository_find_mock = mocker.patch.object(project_dao.repository,
67+
'find',
68+
return_value=fake_project)
69+
70+
response = client.get("/projects/%s" % valid_id, follow_redirects=True)
71+
72+
assert 200 == response.status_code
73+
fake_project == json.loads(response.data)
74+
repository_find_mock.assert_called_once_with(str(valid_id))
75+
76+
77+
def test_get_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
78+
from time_tracker_api.projects.projects_namespace import project_dao
79+
from werkzeug.exceptions import NotFound
80+
81+
invalid_id = fake.random_int(1, 9999)
82+
83+
repository_find_mock = mocker.patch.object(project_dao.repository,
84+
'find',
85+
side_effect=NotFound)
86+
87+
response = client.get("/projects/%s" % invalid_id, follow_redirects=True)
88+
89+
assert 404 == response.status_code
90+
repository_find_mock.assert_called_once_with(str(invalid_id))
91+
92+
93+
def test_get_project_should_return_422_for_invalid_id_format(client: FlaskClient, mocker: MockFixture):
94+
from time_tracker_api.projects.projects_namespace import project_dao
95+
from werkzeug.exceptions import UnprocessableEntity
96+
97+
invalid_id = fake.company()
98+
99+
repository_find_mock = mocker.patch.object(project_dao.repository,
100+
'find',
101+
side_effect=UnprocessableEntity)
102+
103+
response = client.get("/projects/%s" % invalid_id, follow_redirects=True)
104+
105+
assert 422 == response.status_code
106+
repository_find_mock.assert_called_once_with(str(invalid_id))
107+
108+
109+
def update_project_should_succeed_with_valid_data(client: FlaskClient, mocker: MockFixture):
110+
from time_tracker_api.projects.projects_namespace import project_dao
111+
112+
repository_update_mock = mocker.patch.object(project_dao.repository,
113+
'update',
114+
return_value=fake_project)
115+
116+
valid_id = fake.random_int(1, 9999)
117+
response = client.put("/projects/%s" % valid_id, json=valid_project_data, follow_redirects=True)
118+
119+
assert 200 == response.status_code
120+
fake_project == json.loads(response.data)
121+
repository_update_mock.assert_called_once_with(valid_id, valid_project_data)
122+
123+
124+
def test_update_project_should_reject_bad_request(client: FlaskClient, mocker: MockFixture):
125+
from time_tracker_api.projects.projects_namespace import project_dao
126+
invalid_project_data = valid_project_data.copy().update({
127+
"type": 'anything',
128+
})
129+
repository_update_mock = mocker.patch.object(project_dao.repository,
130+
'update',
131+
return_value=fake_project)
132+
133+
valid_id = fake.random_int(1, 9999)
134+
response = client.put("/projects/%s" % valid_id, json=invalid_project_data, follow_redirects=True)
135+
136+
assert 400 == response.status_code
137+
repository_update_mock.assert_not_called()
138+
139+
140+
def test_update_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
141+
from time_tracker_api.projects.projects_namespace import project_dao
142+
from werkzeug.exceptions import NotFound
143+
144+
invalid_id = fake.random_int(1, 9999)
145+
146+
repository_update_mock = mocker.patch.object(project_dao.repository,
147+
'update',
148+
side_effect=NotFound)
149+
150+
response = client.put("/projects/%s" % invalid_id, json=valid_project_data, follow_redirects=True)
151+
152+
assert 404 == response.status_code
153+
repository_update_mock.assert_called_once_with(str(invalid_id), valid_project_data)
154+
155+
156+
def test_delete_project_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture):
157+
from time_tracker_api.projects.projects_namespace import project_dao
158+
159+
valid_id = fake.random_int(1, 9999)
160+
161+
repository_remove_mock = mocker.patch.object(project_dao.repository,
162+
'remove',
163+
return_value=None)
164+
165+
response = client.delete("/projects/%s" % valid_id, follow_redirects=True)
166+
167+
assert 204 == response.status_code
168+
assert b'' == response.data
169+
repository_remove_mock.assert_called_once_with(str(valid_id))
170+
171+
172+
def test_delete_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
173+
from time_tracker_api.projects.projects_namespace import project_dao
174+
from werkzeug.exceptions import NotFound
175+
176+
invalid_id = fake.random_int(1, 9999)
177+
178+
repository_remove_mock = mocker.patch.object(project_dao.repository,
179+
'remove',
180+
side_effect=NotFound)
181+
182+
response = client.delete("/projects/%s" % invalid_id, follow_redirects=True)
183+
184+
assert 404 == response.status_code
185+
repository_remove_mock.assert_called_once_with(str(invalid_id))
186+
187+
188+
def test_delete_project_should_return_422_for_invalid_id_format(client: FlaskClient, mocker: MockFixture):
189+
from time_tracker_api.projects.projects_namespace import project_dao
190+
from werkzeug.exceptions import UnprocessableEntity
191+
192+
invalid_id = fake.company()
193+
194+
repository_remove_mock = mocker.patch.object(project_dao.repository,
195+
'remove',
196+
side_effect=UnprocessableEntity)
197+
198+
response = client.delete("/projects/%s" % invalid_id, follow_redirects=True)
199+
200+
assert 422 == response.status_code
201+
repository_remove_mock.assert_called_once_with(str(invalid_id))

time_tracker_api/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import os
23

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

3536

3637
def init_app(app: Flask):
37-
from .database import init_app as init_database
38+
from time_tracker_api.database import init_app as init_database
3839
init_database(app)
3940

40-
from .api import api
41+
from time_tracker_api.api import api
4142
api.init_app(app)
4243

4344
if app.config.get('DEBUG'):
45+
app.logger.setLevel(logging.INFO)
4446
add_debug_toolbar(app)
4547

4648

49+
4750
def add_debug_toolbar(app):
4851
app.config['DEBUG_TB_PANELS'] = (
4952
'flask_debugtoolbar.panels.versions.VersionDebugPanel',

time_tracker_api/api.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,6 @@
2525
description='Date of update',
2626
example=faker.iso8601(end_datetime=None),
2727
),
28-
# TODO Activate it when the tenants model is implemented
29-
# 'tenant_id': fields.String(
30-
# readOnly=True,
31-
# title='Tenant',
32-
# max_length=64,
33-
# description='The tenant this belongs to',
34-
# example=faker.random_int(1, 9999),
35-
# ),
3628
'created_by': fields.String(
3729
readOnly=True,
3830
title='Creator',

time_tracker_api/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class Config:
77
SECRET_KEY = generate_dev_secret_key()
88
DATABASE_URI = os.environ.get('DATABASE_URI')
99
PROPAGATE_EXCEPTIONS = True
10+
RESTPLUS_VALIDATE = True
1011

1112

1213
class DevelopConfig(Config):

time_tracker_api/projects/projects_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from flask import Flask
44

55
from time_tracker_api.database import CRUDDao
6-
from time_tracker_api.sql_repository import SQLCRUDDao, AuditedSQLModel, SQLModel
76

87

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

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

2525
class ProjectSQLModel(db.Model, SQLModel, AuditedSQLModel):
2626
__tablename__ = 'projects'

time_tracker_api/projects/projects_namespace.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from time_tracker_api import flask_app
55
from time_tracker_api.api import audit_fields
6-
from .projects_model import PROJECT_TYPE, create_dao
6+
from time_tracker_api.projects.projects_model import PROJECT_TYPE, create_dao
77

88
faker = Faker()
99

@@ -65,21 +65,19 @@ class Projects(Resource):
6565
@ns.doc('list_projects')
6666
@ns.marshal_list_with(project, code=200)
6767
def get(self):
68+
"""List all projects"""
6869
return project_dao.get_all(), 200
6970

7071
@ns.doc('create_project')
7172
@ns.response(409, 'This project already exists')
72-
@ns.response(422, 'The data has an invalid format')
73+
@ns.response(400, 'Bad request')
7374
@ns.expect(project_input)
7475
@ns.marshal_with(project, code=201)
7576
def post(self):
77+
"""Create a project"""
7678
return project_dao.create(ns.payload), 201
7779

7880

79-
# TODO : fix, this parser is for a field that is not being used.
80-
project_update_parser = ns.parser()
81-
82-
8381
@ns.route('/<string:id>')
8482
@ns.response(404, 'Project not found')
8583
@ns.param('id', 'The project identifier')
@@ -88,6 +86,7 @@ class Project(Resource):
8886
@ns.response(422, 'The id has an invalid format')
8987
@ns.marshal_with(project)
9088
def get(self, id):
89+
"""Get a project"""
9190
return project_dao.get(id)
9291

9392
@ns.doc('update_project')
@@ -96,11 +95,13 @@ def get(self, id):
9695
@ns.expect(project_input)
9796
@ns.marshal_with(project)
9897
def put(self, id):
98+
"""Update a project"""
9999
return project_dao.update(id, ns.payload)
100100

101101
@ns.doc('delete_project')
102102
@ns.response(204, 'Project deleted successfully')
103103
@ns.response(422, 'The id has an invalid format')
104104
def delete(self, id):
105+
"""Delete a project"""
105106
project_dao.delete(id)
106107
return None, 204

time_tracker_api/sql_repository.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from time_tracker_api.database import CRUDDao, Seeder, DatabaseModel, convert_result_to_dto
77
from time_tracker_api.security import current_user_id
88

9-
db = None
9+
db: SQLAlchemy = None
1010
SQLModel = None
1111
AuditedSQLModel = None
1212

0 commit comments

Comments
 (0)