Skip to content

Commit 3a3ae47

Browse files
author
EliuX
committed
Close #57 Create project type model for Cosmos DB
1 parent a9aca4c commit 3a3ae47

File tree

5 files changed

+264
-42
lines changed

5 files changed

+264
-42
lines changed

tests/time_tracker_api/project_types/__init__.py

Whitespace-only changes.
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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_project_type_data = {
12+
"name": fake.company(),
13+
"description": fake.paragraph(),
14+
'customer_id': fake.uuid4(),
15+
'parent_id': fake.uuid4(),
16+
}
17+
18+
fake_project_type = ({
19+
"id": fake.random_int(1, 9999),
20+
"tenant_id": fake.uuid4(),
21+
}).update(valid_project_type_data)
22+
23+
24+
def test_create_project_type_should_succeed_with_valid_request(client: FlaskClient, mocker: MockFixture):
25+
from time_tracker_api.project_types.project_types_namespace import project_type_dao
26+
repository_create_mock = mocker.patch.object(project_type_dao.repository,
27+
'create',
28+
return_value=fake_project_type)
29+
30+
response = client.post("/project-types", json=valid_project_type_data, follow_redirects=True)
31+
32+
assert HTTPStatus.CREATED == response.status_code
33+
repository_create_mock.assert_called_once()
34+
35+
36+
def test_create_project_type_should_reject_bad_request(client: FlaskClient, mocker: MockFixture):
37+
from time_tracker_api.project_types.project_types_namespace import project_type_dao
38+
invalid_project_type_data = valid_project_type_data.copy()
39+
invalid_project_type_data.update({
40+
"parent_id": None,
41+
})
42+
repository_create_mock = mocker.patch.object(project_type_dao.repository,
43+
'create',
44+
return_value=fake_project_type)
45+
46+
response = client.post("/project-types", json=invalid_project_type_data, follow_redirects=True)
47+
48+
assert HTTPStatus.BAD_REQUEST == response.status_code
49+
repository_create_mock.assert_not_called()
50+
51+
52+
def test_list_all_project_types(client: FlaskClient, mocker: MockFixture):
53+
from time_tracker_api.project_types.project_types_namespace import project_type_dao
54+
repository_find_all_mock = mocker.patch.object(project_type_dao.repository,
55+
'find_all',
56+
return_value=[])
57+
58+
response = client.get("/project-types", follow_redirects=True)
59+
60+
assert HTTPStatus.OK == response.status_code
61+
assert [] == json.loads(response.data)
62+
repository_find_all_mock.assert_called_once()
63+
64+
65+
def test_get_project_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture):
66+
from time_tracker_api.project_types.project_types_namespace import project_type_dao
67+
valid_id = fake.random_int(1, 9999)
68+
repository_find_mock = mocker.patch.object(project_type_dao.repository,
69+
'find',
70+
return_value=fake_project_type)
71+
72+
response = client.get("/project-types/%s" % valid_id, follow_redirects=True)
73+
74+
assert HTTPStatus.OK == response.status_code
75+
fake_project_type == json.loads(response.data)
76+
repository_find_mock.assert_called_once_with(str(valid_id),
77+
partition_key_value=current_user_tenant_id())
78+
79+
80+
def test_get_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
81+
from time_tracker_api.project_types.project_types_namespace import project_type_dao
82+
from werkzeug.exceptions import NotFound
83+
84+
invalid_id = fake.random_int(1, 9999)
85+
86+
repository_find_mock = mocker.patch.object(project_type_dao.repository,
87+
'find',
88+
side_effect=NotFound)
89+
90+
response = client.get("/project-types/%s" % invalid_id, follow_redirects=True)
91+
92+
assert HTTPStatus.NOT_FOUND == response.status_code
93+
repository_find_mock.assert_called_once_with(str(invalid_id),
94+
partition_key_value=current_user_tenant_id())
95+
96+
97+
def test_get_project_should_response_with_unprocessable_entity_for_invalid_id_format(client: FlaskClient,
98+
mocker: MockFixture):
99+
from time_tracker_api.project_types.project_types_namespace import project_type_dao
100+
from werkzeug.exceptions import UnprocessableEntity
101+
102+
invalid_id = fake.company()
103+
104+
repository_find_mock = mocker.patch.object(project_type_dao.repository,
105+
'find',
106+
side_effect=UnprocessableEntity)
107+
108+
response = client.get("/project-types/%s" % invalid_id, follow_redirects=True)
109+
110+
assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code
111+
repository_find_mock.assert_called_once_with(str(invalid_id),
112+
partition_key_value=current_user_tenant_id())
113+
114+
115+
def test_update_project_should_succeed_with_valid_data(client: FlaskClient, mocker: MockFixture):
116+
from time_tracker_api.project_types.project_types_namespace import project_type_dao
117+
118+
repository_update_mock = mocker.patch.object(project_type_dao.repository,
119+
'partial_update',
120+
return_value=fake_project_type)
121+
122+
valid_id = fake.random_int(1, 9999)
123+
response = client.put("/project-types/%s" % valid_id, json=valid_project_type_data, follow_redirects=True)
124+
125+
assert HTTPStatus.OK == response.status_code
126+
fake_project_type == json.loads(response.data)
127+
repository_update_mock.assert_called_once_with(str(valid_id),
128+
changes=valid_project_type_data,
129+
partition_key_value=current_user_tenant_id())
130+
131+
132+
def test_update_project_should_reject_bad_request(client: FlaskClient, mocker: MockFixture):
133+
from time_tracker_api.project_types.project_types_namespace import project_type_dao
134+
invalid_project_type_data = valid_project_type_data.copy()
135+
invalid_project_type_data.update({
136+
"parent_id": None,
137+
})
138+
repository_update_mock = mocker.patch.object(project_type_dao.repository,
139+
'partial_update',
140+
return_value=fake_project_type)
141+
142+
valid_id = fake.random_int(1, 9999)
143+
response = client.put("/project-types/%s" % valid_id, json=invalid_project_type_data, follow_redirects=True)
144+
145+
assert HTTPStatus.BAD_REQUEST == response.status_code
146+
repository_update_mock.assert_not_called()
147+
148+
149+
def test_update_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
150+
from time_tracker_api.project_types.project_types_namespace import project_type_dao
151+
from werkzeug.exceptions import NotFound
152+
153+
invalid_id = fake.random_int(1, 9999)
154+
155+
repository_update_mock = mocker.patch.object(project_type_dao.repository,
156+
'partial_update',
157+
side_effect=NotFound)
158+
159+
response = client.put("/project-types/%s" % invalid_id,
160+
json=valid_project_type_data,
161+
follow_redirects=True)
162+
163+
assert HTTPStatus.NOT_FOUND == response.status_code
164+
repository_update_mock.assert_called_once_with(str(invalid_id),
165+
changes=valid_project_type_data,
166+
partition_key_value=current_user_tenant_id())
167+
168+
169+
def test_delete_project_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture):
170+
from time_tracker_api.project_types.project_types_namespace import project_type_dao
171+
172+
valid_id = fake.random_int(1, 9999)
173+
174+
repository_remove_mock = mocker.patch.object(project_type_dao.repository,
175+
'delete',
176+
return_value=None)
177+
178+
response = client.delete("/project-types/%s" % valid_id, follow_redirects=True)
179+
180+
assert HTTPStatus.NO_CONTENT == response.status_code
181+
assert b'' == response.data
182+
repository_remove_mock.assert_called_once_with(str(valid_id),
183+
partition_key_value=current_user_tenant_id())
184+
185+
186+
def test_delete_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
187+
from time_tracker_api.project_types.project_types_namespace import project_type_dao
188+
from werkzeug.exceptions import NotFound
189+
190+
invalid_id = fake.random_int(1, 9999)
191+
192+
repository_remove_mock = mocker.patch.object(project_type_dao.repository,
193+
'delete',
194+
side_effect=NotFound)
195+
196+
response = client.delete("/project-types/%s" % invalid_id, follow_redirects=True)
197+
198+
assert HTTPStatus.NOT_FOUND == response.status_code
199+
repository_remove_mock.assert_called_once_with(str(invalid_id),
200+
partition_key_value=current_user_tenant_id())
201+
202+
203+
def test_delete_project_should_return_unprocessable_entity_for_invalid_id_format(client: FlaskClient,
204+
mocker: MockFixture):
205+
from time_tracker_api.project_types.project_types_namespace import project_type_dao
206+
from werkzeug.exceptions import UnprocessableEntity
207+
208+
invalid_id = fake.company()
209+
210+
repository_remove_mock = mocker.patch.object(project_type_dao.repository,
211+
'delete',
212+
side_effect=UnprocessableEntity)
213+
214+
response = client.delete("/project-types/%s" % invalid_id, follow_redirects=True)
215+
216+
assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code
217+
repository_remove_mock.assert_called_once_with(str(invalid_id),
218+
partition_key_value=current_user_tenant_id())
Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,52 @@
1+
from dataclasses import dataclass
2+
13
from azure.cosmos import PartitionKey
24

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

58

69
class ProjectTypeDao(CRUDDao):
710
pass
811

912

10-
def create_dao() -> ProjectTypeDao:
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 ProjectTypeSQLModel(db.Model):
18-
__tablename__ = 'project_type'
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-
parent_id = db.Column(UUIDType(binary=False), default=uuid.uuid4)
23-
customer_id = db.Column(UUIDType(binary=False), default=uuid.uuid4)
24-
deleted = db.Column(UUIDType(binary=False), default=uuid.uuid4)
25-
tenant_id = db.Column(UUIDType(binary=False), default=uuid.uuid4)
26-
27-
def __repr__(self):
28-
return '<ProjectType %r>' % self.name
29-
30-
def __str___(self):
31-
return "the project type \"%s\"" % self.name
32-
33-
class ProjectTypeSQLDao(SQLCRUDDao):
34-
def __init__(self):
35-
SQLCRUDDao.__init__(self, ProjectTypeSQLModel)
36-
37-
return ProjectTypeSQLDao()
38-
39-
4013
container_definition = {
4114
'id': 'project_type',
42-
'partition_key': PartitionKey(path='/customer_id'),
15+
'partition_key': PartitionKey(path='/tenant_id'),
4316
'unique_key_policy': {
4417
'uniqueKeys': [
45-
{'paths': ['/name']},
18+
{'paths': ['/name', '/customer_id']},
4619
]
4720
}
4821
}
22+
23+
24+
@dataclass()
25+
class ProjectTypeCosmosDBModel(CosmosDBModel):
26+
id: str
27+
name: str
28+
description: str
29+
parent_id: str
30+
customer_id: str
31+
deleted: str
32+
tenant_id: str
33+
34+
def __init__(self, data):
35+
super(ProjectTypeCosmosDBModel, self).__init__(data) # pragma: no cover
36+
37+
def __repr__(self):
38+
return '<ProjectType %r>' % self.name # pragma: no cover
39+
40+
def __str___(self):
41+
return "the project type \"%s\"" % self.name # pragma: no cover
42+
43+
44+
def create_dao() -> ProjectTypeDao:
45+
repository = CosmosDBRepository.from_definition(container_definition,
46+
mapper=ProjectTypeCosmosDBModel)
47+
48+
class ProjectTypeCosmosDBDao(CosmosDBDao, ProjectTypeDao):
49+
def __init__(self):
50+
CosmosDBDao.__init__(self, repository)
51+
52+
return ProjectTypeCosmosDBDao()

time_tracker_api/project_types/project_types_namespace.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
title='Name',
1717
max_length=50,
1818
description='Name of the project type',
19-
example=faker.company(),
19+
example=faker.random_element(["Customer","Training","Internal"]),
2020
),
2121
'description': fields.String(
2222
title='Description',
@@ -29,17 +29,11 @@
2929
description='Customer this project type belongs to',
3030
example=faker.uuid4(),
3131
),
32-
'tenant_id': fields.String(
33-
required=True,
34-
title='Identifier of Tenant',
35-
description='Tenant this project type belongs to',
36-
example=faker.uuid4(),
37-
),
3832
'parent_id': fields.String(
3933
title='Identifier of Parent of the project type',
4034
description='Defines a self reference of the model ProjectType',
4135
example=faker.uuid4(),
42-
)
36+
),
4337
})
4438

4539
project_type_response_fields = {
@@ -49,7 +43,13 @@
4943
title='Identifier',
5044
description='The unique identifier',
5145
example=faker.uuid4(),
52-
)
46+
),
47+
'tenant_id': fields.String(
48+
required=True,
49+
title='Identifier of Tenant',
50+
description='Tenant this project type belongs to',
51+
example=faker.uuid4(),
52+
),
5353
}
5454
project_type_response_fields.update(common_fields)
5555

time_tracker_api/projects/projects_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class ProjectDao(CRUDDao):
1515
'partition_key': PartitionKey(path='/tenant_id'),
1616
'unique_key_policy': {
1717
'uniqueKeys': [
18-
{'paths': ['/name']},
18+
{'paths': ['/name', '/customer_id']},
1919
]
2020
}
2121
}

0 commit comments

Comments
 (0)