Skip to content

Commit a9aca4c

Browse files
author
EliuX
committed
Close #56 Create customer model for Cosmos DB
1 parent 819ca72 commit a9aca4c

File tree

13 files changed

+272
-55
lines changed

13 files changed

+272
-55
lines changed

setup.cfg

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,9 @@ branch = True
1212
source =
1313
time_tracker_api
1414
commons
15+
16+
[report]
17+
exclude_lines =
18+
pragma: no cover
19+
@dataclass()
20+
raise EnvironmentError

tests/time_tracker_api/customers/__init__.py

Whitespace-only changes.
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
from faker import Faker
2+
from flask import json
3+
from flask.testing import FlaskClient
4+
from flask_restplus._http import HTTPStatus
5+
from pytest_mock import MockFixture
6+
7+
from time_tracker_api.security import current_user_tenant_id
8+
9+
fake = Faker()
10+
11+
valid_customer_data = {
12+
"name": fake.company(),
13+
"description": fake.paragraph(),
14+
"tenant_id": fake.uuid4()
15+
}
16+
17+
fake_customer = ({
18+
"id": fake.random_int(1, 9999)
19+
}).update(valid_customer_data)
20+
21+
22+
def test_create_customer_should_succeed_with_valid_request(client: FlaskClient, mocker: MockFixture):
23+
from time_tracker_api.customers.customers_namespace import customer_dao
24+
repository_create_mock = mocker.patch.object(customer_dao.repository,
25+
'create',
26+
return_value=fake_customer)
27+
28+
response = client.post("/customers", json=valid_customer_data, follow_redirects=True)
29+
30+
assert HTTPStatus.CREATED == response.status_code
31+
repository_create_mock.assert_called_once()
32+
33+
34+
def test_create_customer_should_reject_bad_request(client: FlaskClient, mocker: MockFixture):
35+
from time_tracker_api.customers.customers_namespace import customer_dao
36+
repository_create_mock = mocker.patch.object(customer_dao.repository,
37+
'create',
38+
return_value=fake_customer)
39+
40+
response = client.post("/customers", json=None, follow_redirects=True)
41+
42+
assert HTTPStatus.BAD_REQUEST == response.status_code
43+
repository_create_mock.assert_not_called()
44+
45+
46+
def test_list_all_customers(client: FlaskClient, mocker: MockFixture):
47+
from time_tracker_api.customers.customers_namespace import customer_dao
48+
repository_find_all_mock = mocker.patch.object(customer_dao.repository,
49+
'find_all',
50+
return_value=[])
51+
52+
response = client.get("/customers", follow_redirects=True)
53+
54+
assert HTTPStatus.OK == response.status_code
55+
json_data = json.loads(response.data)
56+
assert [] == json_data
57+
repository_find_all_mock.assert_called_once()
58+
59+
60+
def test_get_customer_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture):
61+
from time_tracker_api.customers.customers_namespace import customer_dao
62+
63+
valid_id = fake.random_int(1, 9999)
64+
65+
repository_find_mock = mocker.patch.object(customer_dao.repository,
66+
'find',
67+
return_value=fake_customer)
68+
69+
response = client.get("/customers/%s" % valid_id, follow_redirects=True)
70+
71+
assert HTTPStatus.OK == response.status_code
72+
fake_customer == json.loads(response.data)
73+
repository_find_mock.assert_called_once_with(str(valid_id),
74+
partition_key_value=current_user_tenant_id())
75+
76+
77+
def test_get_customer_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
78+
from time_tracker_api.customers.customers_namespace import customer_dao
79+
from werkzeug.exceptions import NotFound
80+
81+
invalid_id = fake.random_int(1, 9999)
82+
83+
repository_find_mock = mocker.patch.object(customer_dao.repository,
84+
'find',
85+
side_effect=NotFound)
86+
87+
response = client.get("/customers/%s" % invalid_id, follow_redirects=True)
88+
89+
assert HTTPStatus.NOT_FOUND == response.status_code
90+
repository_find_mock.assert_called_once_with(str(invalid_id),
91+
partition_key_value=current_user_tenant_id())
92+
93+
94+
def test_get_customer_should_return_422_for_invalid_id_format(client: FlaskClient, mocker: MockFixture):
95+
from time_tracker_api.customers.customers_namespace import customer_dao
96+
from werkzeug.exceptions import UnprocessableEntity
97+
98+
invalid_id = fake.company()
99+
100+
repository_find_mock = mocker.patch.object(customer_dao.repository,
101+
'find',
102+
side_effect=UnprocessableEntity)
103+
104+
response = client.get("/customers/%s" % invalid_id, follow_redirects=True)
105+
106+
assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code
107+
repository_find_mock.assert_called_once_with(str(invalid_id),
108+
partition_key_value=current_user_tenant_id())
109+
110+
111+
def test_update_customer_should_succeed_with_valid_data(client: FlaskClient, mocker: MockFixture):
112+
from time_tracker_api.customers.customers_namespace import customer_dao
113+
114+
repository_update_mock = mocker.patch.object(customer_dao.repository,
115+
'partial_update',
116+
return_value=fake_customer)
117+
118+
valid_id = fake.random_int(1, 9999)
119+
response = client.put("/customers/%s" % valid_id, json=valid_customer_data, follow_redirects=True)
120+
121+
assert HTTPStatus.OK == response.status_code
122+
fake_customer == json.loads(response.data)
123+
repository_update_mock.assert_called_once_with(str(valid_id),
124+
changes=valid_customer_data,
125+
partition_key_value=current_user_tenant_id())
126+
127+
128+
def test_update_customer_should_reject_bad_request(client: FlaskClient, mocker: MockFixture):
129+
from time_tracker_api.customers.customers_namespace import customer_dao
130+
repository_update_mock = mocker.patch.object(customer_dao.repository,
131+
'partial_update',
132+
return_value=fake_customer)
133+
134+
valid_id = fake.random_int(1, 9999)
135+
response = client.put("/customers/%s" % valid_id, json=None, follow_redirects=True)
136+
137+
assert HTTPStatus.BAD_REQUEST == response.status_code
138+
repository_update_mock.assert_not_called()
139+
140+
141+
def test_update_customer_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
142+
from time_tracker_api.customers.customers_namespace import customer_dao
143+
from werkzeug.exceptions import NotFound
144+
145+
invalid_id = fake.random_int(1, 9999)
146+
147+
repository_update_mock = mocker.patch.object(customer_dao.repository,
148+
'partial_update',
149+
side_effect=NotFound)
150+
151+
response = client.put("/customers/%s" % invalid_id,
152+
json=valid_customer_data,
153+
follow_redirects=True)
154+
155+
assert HTTPStatus.NOT_FOUND == response.status_code
156+
repository_update_mock.assert_called_once_with(str(invalid_id),
157+
changes=valid_customer_data,
158+
partition_key_value=current_user_tenant_id())
159+
160+
161+
def test_delete_customer_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture):
162+
from time_tracker_api.customers.customers_namespace import customer_dao
163+
164+
valid_id = fake.random_int(1, 9999)
165+
166+
repository_remove_mock = mocker.patch.object(customer_dao.repository,
167+
'delete',
168+
return_value=None)
169+
170+
response = client.delete("/customers/%s" % valid_id, follow_redirects=True)
171+
172+
assert HTTPStatus.NO_CONTENT == response.status_code
173+
assert b'' == response.data
174+
repository_remove_mock.assert_called_once_with(str(valid_id),
175+
partition_key_value=current_user_tenant_id())
176+
177+
178+
def test_delete_customer_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
179+
from time_tracker_api.customers.customers_namespace import customer_dao
180+
from werkzeug.exceptions import NotFound
181+
182+
invalid_id = fake.random_int(1, 9999)
183+
184+
repository_remove_mock = mocker.patch.object(customer_dao.repository,
185+
'delete',
186+
side_effect=NotFound)
187+
188+
response = client.delete("/customers/%s" % invalid_id, follow_redirects=True)
189+
190+
assert HTTPStatus.NOT_FOUND == response.status_code
191+
repository_remove_mock.assert_called_once_with(str(invalid_id),
192+
partition_key_value=current_user_tenant_id())
193+
194+
195+
def test_delete_customer_should_return_422_for_invalid_id_format(client: FlaskClient, mocker: MockFixture):
196+
from time_tracker_api.customers.customers_namespace import customer_dao
197+
from werkzeug.exceptions import UnprocessableEntity
198+
199+
invalid_id = fake.company()
200+
201+
repository_remove_mock = mocker.patch.object(customer_dao.repository,
202+
'delete',
203+
side_effect=UnprocessableEntity)
204+
205+
response = client.delete("/customers/%s" % invalid_id, follow_redirects=True)
206+
207+
assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code
208+
repository_remove_mock.assert_called_once_with(str(invalid_id),
209+
partition_key_value=current_user_tenant_id())

time_tracker_api/activities/activities_model.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ class ActivityCosmosDBModel(CosmosDBModel):
3030
tenant_id: str
3131

3232
def __init__(self, data):
33-
super(ActivityCosmosDBModel, self).__init__(data)
33+
super(ActivityCosmosDBModel, self).__init__(data) # pragma: no cover
3434

3535
def __repr__(self):
36-
return '<Activity %r>' % self.name
36+
return '<Activity %r>' % self.name # pragma: no cover
3737

3838
def __str___(self):
39-
return "the activity \"%s\"" % self.name
39+
return "the activity \"%s\"" % self.name # pragma: no cover
4040

4141

4242
def create_dao() -> ActivityDao:

time_tracker_api/activities/activities_namespace.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from flask_restplus._http import HTTPStatus
44

55
from time_tracker_api.activities.activities_model import create_dao
6-
from time_tracker_api.api import audit_fields
6+
from time_tracker_api.api import common_fields
77

88
faker = Faker()
99

@@ -40,7 +40,7 @@
4040
example=faker.uuid4(),
4141
),
4242
}
43-
activity_response_fields.update(audit_fields)
43+
activity_response_fields.update(common_fields)
4444

4545
activity = ns.inherit(
4646
'Activity',

time_tracker_api/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
)
1616

1717
# Common models structure
18-
audit_fields = {
18+
common_fields = {
1919
'deleted': fields.String(
2020
readOnly=True,
2121
required=True,
Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,50 @@
1+
from dataclasses import dataclass
2+
13
from azure.cosmos import PartitionKey
24

5+
from commons.data_access_layer.cosmos_db import CosmosDBModel, CosmosDBRepository, CosmosDBDao
36
from commons.data_access_layer.database import CRUDDao
47

58

69
class CustomerDao(CRUDDao):
710
pass
811

912

10-
def create_dao() -> CustomerDao:
11-
from commons.data_access_layer.sql import db
12-
from commons.data_access_layer.database import COMMENTS_MAX_LENGTH
13-
from commons.data_access_layer.sql import SQLCRUDDao
14-
from sqlalchemy_utils import UUIDType
15-
import uuid
16-
17-
class CustomerSQLModel(db.Model):
18-
__tablename__ = 'customer'
19-
id = db.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4)
20-
name = db.Column(db.String(50), unique=True, nullable=False)
21-
description = db.Column(db.String(COMMENTS_MAX_LENGTH), unique=False, nullable=False)
22-
deleted = db.Column(UUIDType(binary=False), default=uuid.uuid4)
23-
tenant_id = db.Column(UUIDType(binary=False), default=uuid.uuid4)
24-
25-
def __repr__(self):
26-
return '<Customer %r>' % self.name
27-
28-
def __str___(self):
29-
return "the customer \"%s\"" % self.name
30-
31-
class CustomerSQLDao(SQLCRUDDao):
32-
def __init__(self):
33-
SQLCRUDDao.__init__(self, CustomerSQLModel)
34-
35-
return CustomerSQLDao()
36-
37-
3813
container_definition = {
3914
'id': 'customer',
4015
'partition_key': PartitionKey(path='/tenant_id'),
4116
'unique_key_policy': {
4217
'uniqueKeys': [
4318
{'paths': ['/name']},
44-
{'paths': ['/deleted']},
4519
]
4620
}
4721
}
22+
23+
24+
@dataclass()
25+
class CustomerCosmosDBModel(CosmosDBModel):
26+
id: str
27+
name: str
28+
description: str
29+
deleted: str
30+
tenant_id: str
31+
32+
def __init__(self, data):
33+
super(CustomerCosmosDBModel, self).__init__(data) # pragma: no cover
34+
35+
def __repr__(self):
36+
return '<Customer %r>' % self.name # pragma: no cover
37+
38+
def __str___(self):
39+
return "the customer \"%s\"" % self.name # pragma: no cover
40+
41+
42+
def create_dao() -> CustomerDao:
43+
repository = CosmosDBRepository.from_definition(container_definition,
44+
mapper=CustomerCosmosDBModel)
45+
46+
class CustomerCosmosDBDao(CosmosDBDao, CustomerDao):
47+
def __init__(self):
48+
CosmosDBDao.__init__(self, repository)
49+
50+
return CustomerCosmosDBDao()

time_tracker_api/customers/customers_namespace.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from flask_restplus import Namespace, Resource, fields
33
from flask_restplus._http import HTTPStatus
44

5-
from time_tracker_api.api import audit_fields
5+
from time_tracker_api.api import common_fields
66
from time_tracker_api.customers.customers_model import create_dao
77

88
faker = Faker()
@@ -24,12 +24,6 @@
2424
description='Description about the customer',
2525
example=faker.paragraph(),
2626
),
27-
'tenant_id': fields.String(
28-
required=True,
29-
title='Identifier of Tenant',
30-
description='Tenant this customer belongs to',
31-
example=faker.uuid4(),
32-
),
3327
})
3428

3529
customer_response_fields = {
@@ -39,9 +33,15 @@
3933
title='Identifier',
4034
description='The unique identifier',
4135
example=faker.uuid4(),
42-
)
36+
),
37+
'tenant_id': fields.String(
38+
required=True,
39+
title='Identifier of Tenant',
40+
description='Tenant this customer belongs to',
41+
example=faker.uuid4(),
42+
),
4343
}
44-
customer_response_fields.update(audit_fields)
44+
customer_response_fields.update(common_fields)
4545

4646
customer = ns.inherit(
4747
'Customer',

0 commit comments

Comments
 (0)