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
Prev Previous commit
Next Next commit
Close #57 Create project type model for Cosmos DB
  • Loading branch information
EliuX committed Apr 14, 2020
commit 3a3ae47f4ba3dc7930eee0104070600b1305e9b9
Empty file.
218 changes: 218 additions & 0 deletions tests/time_tracker_api/project_types/project_types_namespace_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
from faker import Faker
from flask import json
from flask.testing import FlaskClient
from flask_restplus._http import HTTPStatus
from pytest_mock import MockFixture

from time_tracker_api.security import current_user_tenant_id

fake = Faker()

valid_project_type_data = {
"name": fake.company(),
"description": fake.paragraph(),
'customer_id': fake.uuid4(),
'parent_id': fake.uuid4(),
}

fake_project_type = ({
"id": fake.random_int(1, 9999),
"tenant_id": fake.uuid4(),
}).update(valid_project_type_data)


def test_create_project_type_should_succeed_with_valid_request(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.project_types.project_types_namespace import project_type_dao
repository_create_mock = mocker.patch.object(project_type_dao.repository,
'create',
return_value=fake_project_type)

response = client.post("/project-types", json=valid_project_type_data, follow_redirects=True)

assert HTTPStatus.CREATED == response.status_code
repository_create_mock.assert_called_once()


def test_create_project_type_should_reject_bad_request(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.project_types.project_types_namespace import project_type_dao
invalid_project_type_data = valid_project_type_data.copy()
invalid_project_type_data.update({
"parent_id": None,
})
repository_create_mock = mocker.patch.object(project_type_dao.repository,
'create',
return_value=fake_project_type)

response = client.post("/project-types", json=invalid_project_type_data, follow_redirects=True)

assert HTTPStatus.BAD_REQUEST == response.status_code
repository_create_mock.assert_not_called()


def test_list_all_project_types(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.project_types.project_types_namespace import project_type_dao
repository_find_all_mock = mocker.patch.object(project_type_dao.repository,
'find_all',
return_value=[])

response = client.get("/project-types", follow_redirects=True)

assert HTTPStatus.OK == response.status_code
assert [] == json.loads(response.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.project_types.project_types_namespace import project_type_dao
valid_id = fake.random_int(1, 9999)
repository_find_mock = mocker.patch.object(project_type_dao.repository,
'find',
return_value=fake_project_type)

response = client.get("/project-types/%s" % valid_id, follow_redirects=True)

assert HTTPStatus.OK == response.status_code
fake_project_type == json.loads(response.data)
repository_find_mock.assert_called_once_with(str(valid_id),
partition_key_value=current_user_tenant_id())


def test_get_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.project_types.project_types_namespace import project_type_dao
from werkzeug.exceptions import NotFound

invalid_id = fake.random_int(1, 9999)

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

response = client.get("/project-types/%s" % invalid_id, follow_redirects=True)

assert HTTPStatus.NOT_FOUND == response.status_code
repository_find_mock.assert_called_once_with(str(invalid_id),
partition_key_value=current_user_tenant_id())


def test_get_project_should_response_with_unprocessable_entity_for_invalid_id_format(client: FlaskClient,
mocker: MockFixture):
from time_tracker_api.project_types.project_types_namespace import project_type_dao
from werkzeug.exceptions import UnprocessableEntity

invalid_id = fake.company()

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

response = client.get("/project-types/%s" % invalid_id, follow_redirects=True)

assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code
repository_find_mock.assert_called_once_with(str(invalid_id),
partition_key_value=current_user_tenant_id())


def test_update_project_should_succeed_with_valid_data(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.project_types.project_types_namespace import project_type_dao

repository_update_mock = mocker.patch.object(project_type_dao.repository,
'partial_update',
return_value=fake_project_type)

valid_id = fake.random_int(1, 9999)
response = client.put("/project-types/%s" % valid_id, json=valid_project_type_data, follow_redirects=True)

assert HTTPStatus.OK == response.status_code
fake_project_type == json.loads(response.data)
repository_update_mock.assert_called_once_with(str(valid_id),
changes=valid_project_type_data,
partition_key_value=current_user_tenant_id())


def test_update_project_should_reject_bad_request(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.project_types.project_types_namespace import project_type_dao
invalid_project_type_data = valid_project_type_data.copy()
invalid_project_type_data.update({
"parent_id": None,
})
repository_update_mock = mocker.patch.object(project_type_dao.repository,
'partial_update',
return_value=fake_project_type)

valid_id = fake.random_int(1, 9999)
response = client.put("/project-types/%s" % valid_id, json=invalid_project_type_data, follow_redirects=True)

assert HTTPStatus.BAD_REQUEST == 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.project_types.project_types_namespace import project_type_dao
from werkzeug.exceptions import NotFound

invalid_id = fake.random_int(1, 9999)

repository_update_mock = mocker.patch.object(project_type_dao.repository,
'partial_update',
side_effect=NotFound)

response = client.put("/project-types/%s" % invalid_id,
json=valid_project_type_data,
follow_redirects=True)

assert HTTPStatus.NOT_FOUND == response.status_code
repository_update_mock.assert_called_once_with(str(invalid_id),
changes=valid_project_type_data,
partition_key_value=current_user_tenant_id())


def test_delete_project_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.project_types.project_types_namespace import project_type_dao

valid_id = fake.random_int(1, 9999)

repository_remove_mock = mocker.patch.object(project_type_dao.repository,
'delete',
return_value=None)

response = client.delete("/project-types/%s" % valid_id, follow_redirects=True)

assert HTTPStatus.NO_CONTENT == response.status_code
assert b'' == response.data
repository_remove_mock.assert_called_once_with(str(valid_id),
partition_key_value=current_user_tenant_id())


def test_delete_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
from time_tracker_api.project_types.project_types_namespace import project_type_dao
from werkzeug.exceptions import NotFound

invalid_id = fake.random_int(1, 9999)

repository_remove_mock = mocker.patch.object(project_type_dao.repository,
'delete',
side_effect=NotFound)

response = client.delete("/project-types/%s" % invalid_id, follow_redirects=True)

assert HTTPStatus.NOT_FOUND == response.status_code
repository_remove_mock.assert_called_once_with(str(invalid_id),
partition_key_value=current_user_tenant_id())


def test_delete_project_should_return_unprocessable_entity_for_invalid_id_format(client: FlaskClient,
mocker: MockFixture):
from time_tracker_api.project_types.project_types_namespace import project_type_dao
from werkzeug.exceptions import UnprocessableEntity

invalid_id = fake.company()

repository_remove_mock = mocker.patch.object(project_type_dao.repository,
'delete',
side_effect=UnprocessableEntity)

response = client.delete("/project-types/%s" % invalid_id, follow_redirects=True)

assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code
repository_remove_mock.assert_called_once_with(str(invalid_id),
partition_key_value=current_user_tenant_id())
68 changes: 36 additions & 32 deletions time_tracker_api/project_types/project_types_model.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,52 @@
from dataclasses import dataclass

from azure.cosmos import PartitionKey

from commons.data_access_layer.cosmos_db import CosmosDBModel, CosmosDBDao, CosmosDBRepository
from commons.data_access_layer.database import CRUDDao


class ProjectTypeDao(CRUDDao):
pass


def create_dao() -> ProjectTypeDao:
from commons.data_access_layer.sql import db
from commons.data_access_layer.database import COMMENTS_MAX_LENGTH
from commons.data_access_layer.sql import SQLCRUDDao
from sqlalchemy_utils import UUIDType
import uuid

class ProjectTypeSQLModel(db.Model):
__tablename__ = 'project_type'
id = db.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4)
name = db.Column(db.String(50), unique=True, nullable=False)
description = db.Column(db.String(COMMENTS_MAX_LENGTH), unique=False, nullable=False)
parent_id = db.Column(UUIDType(binary=False), default=uuid.uuid4)
customer_id = db.Column(UUIDType(binary=False), default=uuid.uuid4)
deleted = db.Column(UUIDType(binary=False), default=uuid.uuid4)
tenant_id = db.Column(UUIDType(binary=False), default=uuid.uuid4)

def __repr__(self):
return '<ProjectType %r>' % self.name

def __str___(self):
return "the project type \"%s\"" % self.name

class ProjectTypeSQLDao(SQLCRUDDao):
def __init__(self):
SQLCRUDDao.__init__(self, ProjectTypeSQLModel)

return ProjectTypeSQLDao()


container_definition = {
'id': 'project_type',
'partition_key': PartitionKey(path='/customer_id'),
'partition_key': PartitionKey(path='/tenant_id'),
'unique_key_policy': {
'uniqueKeys': [
{'paths': ['/name']},
{'paths': ['/name', '/customer_id']},
]
}
}


@dataclass()
class ProjectTypeCosmosDBModel(CosmosDBModel):
id: str
name: str
description: str
parent_id: str
customer_id: str
deleted: str
tenant_id: str

def __init__(self, data):
super(ProjectTypeCosmosDBModel, self).__init__(data) # pragma: no cover

def __repr__(self):
return '<ProjectType %r>' % self.name # pragma: no cover

def __str___(self):
return "the project type \"%s\"" % self.name # pragma: no cover


def create_dao() -> ProjectTypeDao:
repository = CosmosDBRepository.from_definition(container_definition,
mapper=ProjectTypeCosmosDBModel)

class ProjectTypeCosmosDBDao(CosmosDBDao, ProjectTypeDao):
def __init__(self):
CosmosDBDao.__init__(self, repository)

return ProjectTypeCosmosDBDao()
18 changes: 9 additions & 9 deletions time_tracker_api/project_types/project_types_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
title='Name',
max_length=50,
description='Name of the project type',
example=faker.company(),
example=faker.random_element(["Customer","Training","Internal"]),
),
'description': fields.String(
title='Description',
Expand All @@ -29,17 +29,11 @@
description='Customer this project type belongs to',
example=faker.uuid4(),
),
'tenant_id': fields.String(
required=True,
title='Identifier of Tenant',
description='Tenant this project type belongs to',
example=faker.uuid4(),
),
'parent_id': fields.String(
title='Identifier of Parent of the project type',
description='Defines a self reference of the model ProjectType',
example=faker.uuid4(),
)
),
})

project_type_response_fields = {
Expand All @@ -49,7 +43,13 @@
title='Identifier',
description='The unique identifier',
example=faker.uuid4(),
)
),
'tenant_id': fields.String(
required=True,
title='Identifier of Tenant',
description='Tenant this project type belongs to',
example=faker.uuid4(),
),
}
project_type_response_fields.update(common_fields)

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 @@ -15,7 +15,7 @@ class ProjectDao(CRUDDao):
'partition_key': PartitionKey(path='/tenant_id'),
'unique_key_policy': {
'uniqueKeys': [
{'paths': ['/name']},
{'paths': ['/name', '/customer_id']},
]
}
}
Expand Down