Skip to content

Commit 36f239a

Browse files
committed
feat: technologies endpoint #200
1 parent 91fc649 commit 36f239a

File tree

6 files changed

+412
-11
lines changed

6 files changed

+412
-11
lines changed

migrations/03-add-technologies.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
def up():
2+
from commons.data_access_layer.cosmos_db import cosmos_helper
3+
import azure.cosmos.exceptions as exceptions
4+
from . import app
5+
6+
app.logger.info("Creating technology container...")
7+
8+
try:
9+
app.logger.info('- Technologies')
10+
from time_tracker_api.technologies.technologies_model import (
11+
container_definition as technologies_definition,
12+
)
13+
14+
cosmos_helper.create_container(technologies_definition)
15+
16+
except exceptions.CosmosResourceExistsError as e:
17+
app.logger.warning(
18+
"Unexpected error while creating initial database schema: %s"
19+
% e.message
20+
)
21+
22+
app.logger.info("Done!")
23+
24+
25+
def down():
26+
print("Not implemented!")

tests/time_tracker_api/technologies/__init__.py

Whitespace-only changes.
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
from unittest.mock import ANY, Mock
2+
3+
import pytest
4+
5+
from faker import Faker
6+
from flask.testing import FlaskClient
7+
from flask_restplus._http import HTTPStatus
8+
9+
from werkzeug.exceptions import NotFound, UnprocessableEntity, HTTPException
10+
11+
12+
@pytest.fixture
13+
def technology_dao():
14+
from time_tracker_api.technologies.technologies_namespace import (
15+
technology_dao,
16+
)
17+
18+
return technology_dao
19+
20+
21+
def test_list_all_technologies(
22+
client: FlaskClient, valid_header: dict, technology_dao
23+
):
24+
technology_dao.repository.find_all = Mock(return_value=[])
25+
response = client.get("/technologies", headers=valid_header)
26+
assert HTTPStatus.OK == response.status_code
27+
28+
29+
def test_get_technology_suceeds_with_valid_id(
30+
client: FlaskClient, valid_header: dict, technology_dao
31+
):
32+
technology_dao.repository.find = Mock(return_value={})
33+
id: str = Faker().uuid4()
34+
35+
response = client.get(f"/technologies/{id}", headers=valid_header)
36+
37+
assert HTTPStatus.OK == response.status_code
38+
technology_dao.repository.find.assert_called_once_with(id, ANY)
39+
40+
41+
def test_create_technology_suceeds_with_valid_input(
42+
client: FlaskClient, valid_header: dict, technology_dao
43+
):
44+
technology_dao.repository.create = Mock(return_value={})
45+
payload = {
46+
'name': 'neo4j',
47+
'creation_date': '2020-04-01T05:00:00+00:00',
48+
'first_use_time_entry_id': Faker().uuid4(),
49+
}
50+
51+
response = client.post("/technologies", headers=valid_header, json=payload)
52+
53+
assert HTTPStatus.CREATED == response.status_code
54+
technology_dao.repository.create.assert_called_once()
55+
56+
57+
def test_create_technology_fails_with_invalid_input(
58+
client: FlaskClient, valid_header: dict, technology_dao
59+
):
60+
technology_dao.repository.create = Mock(return_value={})
61+
62+
response = client.post("/technologies", headers=valid_header, json={})
63+
64+
assert HTTPStatus.BAD_REQUEST == response.status_code
65+
technology_dao.repository.create.assert_not_called()
66+
67+
68+
def test_update_technology_succeeds_with_valid_input(
69+
client: FlaskClient, valid_header: dict, technology_dao
70+
):
71+
72+
technology_dao.repository.partial_update = Mock({})
73+
id: str = Faker().uuid4()
74+
payload = {
75+
'name': 'neo4j',
76+
'creation_date': '2020-04-01T05:00:00+00:00',
77+
'first_use_time_entry_id': Faker().uuid4(),
78+
}
79+
80+
response = client.put(
81+
f"/technologies/{id}", headers=valid_header, json=payload
82+
)
83+
84+
assert HTTPStatus.OK == response.status_code
85+
technology_dao.repository.partial_update.assert_called_once_with(
86+
id, payload, ANY
87+
)
88+
89+
90+
def test_update_technology_fails_with_invalid_input(
91+
client: FlaskClient, valid_header: dict, technology_dao
92+
):
93+
94+
technology_dao.repository.partial_update = Mock({})
95+
id: str = Faker().uuid4()
96+
97+
response = client.put(
98+
f"/technologies/{id}", headers=valid_header, json=None
99+
)
100+
101+
assert HTTPStatus.BAD_REQUEST == response.status_code
102+
technology_dao.repository.partial_update.assert_not_called()
103+
104+
105+
def test_delete_technology_suceeds(
106+
client: FlaskClient, valid_header: dict, technology_dao
107+
):
108+
technology_dao.repository.delete = Mock(None)
109+
id: str = Faker().uuid4()
110+
111+
response = client.delete(f'/technologies/{id}', headers=valid_header)
112+
113+
assert HTTPStatus.NO_CONTENT == response.status_code
114+
assert b'' == response.data
115+
technology_dao.repository.delete.assert_called_once_with(id, ANY)
116+
117+
118+
@pytest.mark.parametrize(
119+
'http_exception,http_status',
120+
[
121+
(NotFound, HTTPStatus.NOT_FOUND),
122+
(UnprocessableEntity, HTTPStatus.UNPROCESSABLE_ENTITY),
123+
],
124+
)
125+
def test_get_technology_raise_http_exception_on_error(
126+
client: FlaskClient,
127+
valid_header: dict,
128+
http_exception: HTTPException,
129+
http_status: tuple,
130+
technology_dao,
131+
):
132+
133+
technology_dao.repository.find = Mock(side_effect=http_exception)
134+
id: str = Faker().uuid4()
135+
136+
response = client.get(f"/technologies/{id}", headers=valid_header)
137+
138+
assert http_status == response.status_code
139+
technology_dao.repository.find.assert_called_once_with(id, ANY)
140+
141+
142+
@pytest.mark.parametrize(
143+
'http_exception,http_status',
144+
[
145+
(NotFound, HTTPStatus.NOT_FOUND),
146+
(UnprocessableEntity, HTTPStatus.UNPROCESSABLE_ENTITY),
147+
],
148+
)
149+
def test_delete_technology_raise_http_exception_on_error(
150+
client: FlaskClient,
151+
valid_header: dict,
152+
http_exception: HTTPException,
153+
http_status: tuple,
154+
technology_dao,
155+
):
156+
157+
technology_dao.repository.delete = Mock(side_effect=http_exception)
158+
id: str = Faker().uuid4()
159+
160+
response = client.delete(f"/technologies/{id}", headers=valid_header)
161+
162+
assert http_status == response.status_code
163+
technology_dao.repository.delete.assert_called_once_with(id, ANY)
164+
165+
166+
@pytest.mark.parametrize(
167+
'http_exception,http_status',
168+
[
169+
(NotFound, HTTPStatus.NOT_FOUND),
170+
(UnprocessableEntity, HTTPStatus.UNPROCESSABLE_ENTITY),
171+
],
172+
)
173+
def test_update_technology_raise_http_exception_on_error(
174+
client: FlaskClient,
175+
valid_header: dict,
176+
http_exception: HTTPException,
177+
http_status: tuple,
178+
technology_dao,
179+
):
180+
181+
technology_dao.repository.partial_update = Mock(side_effect=http_exception)
182+
id: str = Faker().uuid4()
183+
payload = {}
184+
185+
response = client.put(
186+
f"/technologies/{id}", headers=valid_header, json=payload
187+
)
188+
189+
assert http_status == response.status_code
190+
technology_dao.repository.partial_update.assert_called_once_with(
191+
id, payload, ANY
192+
)

time_tracker_api/api.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
from azure.cosmos.exceptions import CosmosResourceExistsError, CosmosResourceNotFoundError, CosmosHttpResponseError
1+
from azure.cosmos.exceptions import (
2+
CosmosResourceExistsError,
3+
CosmosResourceNotFoundError,
4+
CosmosHttpResponseError,
5+
)
26
from faker import Faker
37
from flask import current_app as app, Flask
48
from flask_restplus import Api, fields, Model
@@ -30,17 +34,24 @@ def remove_required_constraint(model: Model):
3034
return result
3135

3236

33-
def create_attributes_filter(ns: namespace, model: Model, filter_attrib_names: list) -> RequestParser:
37+
def create_attributes_filter(
38+
ns: namespace, model: Model, filter_attrib_names: list
39+
) -> RequestParser:
3440
attribs_parser = ns.parser()
3541
model_attributes = model.resolved
3642
for attrib in filter_attrib_names:
3743
if attrib not in model_attributes:
38-
raise ValueError(f"{attrib} is not a valid filter attribute for {model.name}")
39-
40-
attribs_parser.add_argument(attrib, required=False,
41-
store_missing=False,
42-
help="(Filter) %s " % model_attributes[attrib].description,
43-
location='args')
44+
raise ValueError(
45+
f"{attrib} is not a valid filter attribute for {model.name}"
46+
)
47+
48+
attribs_parser.add_argument(
49+
attrib,
50+
required=False,
51+
store_missing=False,
52+
help="(Filter) %s " % model_attributes[attrib].description,
53+
location='args',
54+
)
4455

4556
return attribs_parser
4657

@@ -103,6 +114,10 @@ def init_app(app: Flask):
103114

104115
api.add_namespace(customers_namespace.ns)
105116

117+
from time_tracker_api.technologies import technologies_namespace
118+
119+
api.add_namespace(technologies_namespace.ns)
120+
106121

107122
"""
108123
Error handlers
@@ -122,12 +137,18 @@ def handle_not_found_errors(error):
122137

123138
@api.errorhandler(CosmosHttpResponseError)
124139
def handle_cosmos_http_response_error(error):
125-
return {'message': 'Invalid request. Please verify your data.'}, HTTPStatus.BAD_REQUEST
140+
return (
141+
{'message': 'Invalid request. Please verify your data.'},
142+
HTTPStatus.BAD_REQUEST,
143+
)
126144

127145

128146
@api.errorhandler(AttributeError)
129147
def handle_attribute_error(error):
130-
return {'message': "There are missing attributes"}, HTTPStatus.UNPROCESSABLE_ENTITY
148+
return (
149+
{'message': "There are missing attributes"},
150+
HTTPStatus.UNPROCESSABLE_ENTITY,
151+
)
131152

132153

133154
@api.errorhandler(CustomError)
@@ -138,4 +159,7 @@ def handle_custom_error(error):
138159
@api.errorhandler
139160
def default_error_handler(error):
140161
app.logger.error(error)
141-
return {'message': 'An unhandled exception occurred.'}, HTTPStatus.INTERNAL_SERVER_ERROR
162+
return (
163+
{'message': 'An unhandled exception occurred.'},
164+
HTTPStatus.INTERNAL_SERVER_ERROR,
165+
)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from dataclasses import dataclass
2+
3+
from azure.cosmos import PartitionKey
4+
5+
from commons.data_access_layer.cosmos_db import (
6+
CosmosDBModel,
7+
CosmosDBDao,
8+
CosmosDBRepository,
9+
)
10+
from time_tracker_api.database import CRUDDao, APICosmosDBDao
11+
12+
13+
class TechnologyDao(CRUDDao):
14+
pass
15+
16+
17+
container_definition = {
18+
'id': 'technology',
19+
'partition_key': PartitionKey(path='/tenant_id'),
20+
'unique_key_policy': {'uniqueKeys': [{'paths': ['/name']},]},
21+
}
22+
23+
24+
@dataclass()
25+
class TechnologyCosmosDBModel(CosmosDBModel):
26+
id: str
27+
name: str
28+
first_use_time_entry_id: str
29+
creation_date: str
30+
deleted: str
31+
tenant_id: str
32+
33+
def __init__(self, data):
34+
super(TechnologyCosmosDBModel, self).__init__(data) # pragma: no cover
35+
36+
def __repr__(self):
37+
return '<Technology %r>' % self.name # pragma: no cover
38+
39+
def __str___(self):
40+
return "the Technology \"%s\"" % self.name # pragma: no cover
41+
42+
43+
def create_dao() -> TechnologyDao:
44+
repository = CosmosDBRepository.from_definition(
45+
container_definition, mapper=TechnologyCosmosDBModel
46+
)
47+
48+
class TechnologyCosmosDBDao(APICosmosDBDao, TechnologyDao):
49+
def __init__(self):
50+
CosmosDBDao.__init__(self, repository)
51+
52+
return TechnologyCosmosDBDao()

0 commit comments

Comments
 (0)