diff --git a/.gitignore b/.gitignore index b4e0b071..0fd170bc 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,6 @@ migration_status.csv .DS_Store # windows env variables -.env.bat \ No newline at end of file +.env.bat +# mac / linux env variables +.env \ No newline at end of file diff --git a/tests/time_tracker_api/customers/customers_namespace_test.py b/tests/time_tracker_api/customers/customers_namespace_test.py index 8d844782..9c8bfa07 100644 --- a/tests/time_tracker_api/customers/customers_namespace_test.py +++ b/tests/time_tracker_api/customers/customers_namespace_test.py @@ -11,59 +11,65 @@ valid_customer_data = { "name": fake.company(), "description": fake.paragraph(), - "tenant_id": fake.uuid4() + "tenant_id": fake.uuid4(), } -fake_customer = ({ - "id": fake.random_int(1, 9999), -}).update(valid_customer_data) +fake_customer = ( + { + "id": fake.random_int(1, 9999), + } +).update(valid_customer_data) -def test_create_customer_should_succeed_with_valid_request(client: FlaskClient, - mocker: MockFixture, - valid_header: dict): +def test_create_customer_should_succeed_with_valid_request( + client: FlaskClient, mocker: MockFixture, valid_header: dict +): from time_tracker_api.customers.customers_namespace import customer_dao - repository_create_mock = mocker.patch.object(customer_dao.repository, - 'create', - return_value=fake_customer) - response = client.post("/customers", - headers=valid_header, - json=valid_customer_data, - follow_redirects=True) + repository_create_mock = mocker.patch.object( + customer_dao.repository, 'create', return_value=fake_customer + ) + + response = client.post( + "/customers", + headers=valid_header, + json=valid_customer_data, + follow_redirects=True, + ) assert HTTPStatus.CREATED == response.status_code repository_create_mock.assert_called_once() -def test_create_customer_should_reject_bad_request(client: FlaskClient, - mocker: MockFixture, - valid_header: dict): +def test_create_customer_should_reject_bad_request( + client: FlaskClient, mocker: MockFixture, valid_header: dict +): from time_tracker_api.customers.customers_namespace import customer_dao - repository_create_mock = mocker.patch.object(customer_dao.repository, - 'create', - return_value=fake_customer) - response = client.post("/customers", - headers=valid_header, - json=None, - follow_redirects=True) + repository_create_mock = mocker.patch.object( + customer_dao.repository, 'create', return_value=fake_customer + ) + + response = client.post( + "/customers", headers=valid_header, json=None, follow_redirects=True + ) assert HTTPStatus.BAD_REQUEST == response.status_code repository_create_mock.assert_not_called() -def test_list_all_customers(client: FlaskClient, - mocker: MockFixture, - valid_header: dict): +def test_list_all_customers( + client: FlaskClient, mocker: MockFixture, valid_header: dict +): from time_tracker_api.customers.customers_namespace import customer_dao - repository_find_all_mock = mocker.patch.object(customer_dao.repository, - 'find_all', - return_value=[]) - response = client.get("/customers", - headers=valid_header, - follow_redirects=True) + repository_find_all_mock = mocker.patch.object( + customer_dao.repository, 'find_all', return_value=[] + ) + + response = client.get( + "/customers", headers=valid_header, follow_redirects=True + ) assert HTTPStatus.OK == response.status_code json_data = json.loads(response.data) @@ -71,181 +77,246 @@ def test_list_all_customers(client: FlaskClient, repository_find_all_mock.assert_called_once() -def test_get_customer_should_succeed_with_valid_id(client: FlaskClient, - mocker: MockFixture, - valid_header: dict): +def test_get_customer_should_succeed_with_valid_id( + client: FlaskClient, mocker: MockFixture, valid_header: dict +): from time_tracker_api.customers.customers_namespace import customer_dao valid_id = fake.uuid4() - repository_find_mock = mocker.patch.object(customer_dao.repository, - 'find', - return_value=fake_customer) + repository_find_mock = mocker.patch.object( + customer_dao.repository, 'find', return_value=fake_customer + ) - response = client.get("/customers/%s" % valid_id, - headers=valid_header, - follow_redirects=True) + response = client.get( + "/customers/%s" % valid_id, headers=valid_header, follow_redirects=True + ) assert HTTPStatus.OK == response.status_code fake_customer == json.loads(response.data) repository_find_mock.assert_called_once_with(str(valid_id), ANY) -def test_get_customer_should_return_not_found_with_invalid_id(client: FlaskClient, - mocker: MockFixture, - valid_header: dict): +def test_get_customer_should_return_not_found_with_invalid_id( + client: FlaskClient, mocker: MockFixture, valid_header: dict +): from time_tracker_api.customers.customers_namespace import customer_dao from werkzeug.exceptions import NotFound invalid_id = fake.random_int(1, 9999) - repository_find_mock = mocker.patch.object(customer_dao.repository, - 'find', - side_effect=NotFound) + repository_find_mock = mocker.patch.object( + customer_dao.repository, 'find', side_effect=NotFound + ) - response = client.get("/customers/%s" % invalid_id, - headers=valid_header, - follow_redirects=True) + response = client.get( + "/customers/%s" % invalid_id, + headers=valid_header, + follow_redirects=True, + ) assert HTTPStatus.NOT_FOUND == response.status_code repository_find_mock.assert_called_once_with(str(invalid_id), ANY) -def test_get_customer_should_return_422_for_invalid_id_format(client: FlaskClient, - mocker: MockFixture, - valid_header: dict): +def test_get_customer_should_return_422_for_invalid_id_format( + client: FlaskClient, mocker: MockFixture, valid_header: dict +): from time_tracker_api.customers.customers_namespace import customer_dao from werkzeug.exceptions import UnprocessableEntity invalid_id = fake.company() - repository_find_mock = mocker.patch.object(customer_dao.repository, - 'find', - side_effect=UnprocessableEntity) + repository_find_mock = mocker.patch.object( + customer_dao.repository, 'find', side_effect=UnprocessableEntity + ) - response = client.get("/customers/%s" % invalid_id, - headers=valid_header, - follow_redirects=True) + response = client.get( + "/customers/%s" % invalid_id, + headers=valid_header, + follow_redirects=True, + ) assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code repository_find_mock.assert_called_once_with(str(invalid_id), ANY) -def test_update_customer_should_succeed_with_valid_data(client: FlaskClient, - mocker: MockFixture, - valid_header: dict): +def test_update_customer_should_succeed_with_valid_data( + client: FlaskClient, mocker: MockFixture, valid_header: dict +): from time_tracker_api.customers.customers_namespace import customer_dao - repository_update_mock = mocker.patch.object(customer_dao.repository, - 'partial_update', - return_value=fake_customer) + repository_update_mock = mocker.patch.object( + customer_dao.repository, 'partial_update', return_value=fake_customer + ) valid_id = fake.random_int(1, 9999) - response = client.put("/customers/%s" % valid_id, - headers=valid_header, - json=valid_customer_data, - follow_redirects=True) + response = client.put( + "/customers/%s" % valid_id, + headers=valid_header, + json=valid_customer_data, + follow_redirects=True, + ) assert HTTPStatus.OK == response.status_code fake_customer == json.loads(response.data) - repository_update_mock.assert_called_once_with(str(valid_id), valid_customer_data, ANY) + repository_update_mock.assert_called_once_with( + str(valid_id), valid_customer_data, ANY + ) -def test_update_customer_should_reject_bad_request(client: FlaskClient, - mocker: MockFixture, - valid_header: dict): +def test_update_customer_should_reject_bad_request( + client: FlaskClient, mocker: MockFixture, valid_header: dict +): from time_tracker_api.customers.customers_namespace import customer_dao - repository_update_mock = mocker.patch.object(customer_dao.repository, - 'partial_update', - return_value=fake_customer) + + repository_update_mock = mocker.patch.object( + customer_dao.repository, 'partial_update', return_value=fake_customer + ) valid_id = fake.random_int(1, 9999) - response = client.put("/customers/%s" % valid_id, - headers=valid_header, - json=None, - follow_redirects=True) + response = client.put( + "/customers/%s" % valid_id, + headers=valid_header, + json=None, + follow_redirects=True, + ) assert HTTPStatus.BAD_REQUEST == response.status_code repository_update_mock.assert_not_called() -def test_update_customer_should_return_not_found_with_invalid_id(client: FlaskClient, - mocker: MockFixture, - valid_header: dict): +def test_update_customer_should_return_not_found_with_invalid_id( + client: FlaskClient, mocker: MockFixture, valid_header: dict +): from time_tracker_api.customers.customers_namespace import customer_dao from werkzeug.exceptions import NotFound invalid_id = fake.random_int(1, 9999) - repository_update_mock = mocker.patch.object(customer_dao.repository, - 'partial_update', - side_effect=NotFound) + repository_update_mock = mocker.patch.object( + customer_dao.repository, 'partial_update', side_effect=NotFound + ) - response = client.put("/customers/%s" % invalid_id, - headers=valid_header, - json=valid_customer_data, - follow_redirects=True) + response = client.put( + "/customers/%s" % invalid_id, + headers=valid_header, + json=valid_customer_data, + follow_redirects=True, + ) assert HTTPStatus.NOT_FOUND == response.status_code - repository_update_mock.assert_called_once_with(str(invalid_id), valid_customer_data, ANY) + repository_update_mock.assert_called_once_with( + str(invalid_id), valid_customer_data, ANY + ) -def test_delete_customer_should_succeed_with_valid_id(client: FlaskClient, - mocker: MockFixture, - valid_header: dict): +def test_delete_customer_should_succeed_with_valid_id( + client: FlaskClient, mocker: MockFixture, valid_header: dict +): from time_tracker_api.customers.customers_namespace import customer_dao valid_id = fake.random_int(1, 9999) - repository_remove_mock = mocker.patch.object(customer_dao.repository, - 'delete', - return_value=None) + repository_remove_mock = mocker.patch.object( + customer_dao.repository, 'partial_update', return_value=None + ) - response = client.delete("/customers/%s" % valid_id, - headers=valid_header, - follow_redirects=True) + response = client.delete( + "/customers/%s" % valid_id, headers=valid_header, follow_redirects=True + ) assert HTTPStatus.NO_CONTENT == response.status_code assert b'' == response.data - repository_remove_mock.assert_called_once_with(str(valid_id), ANY) + repository_remove_mock.assert_called_once_with( + str(valid_id), {'status': 'inactive'}, ANY + ) -def test_delete_customer_should_return_not_found_with_invalid_id(client: FlaskClient, - mocker: MockFixture, - valid_header: dict): +def test_delete_customer_should_return_not_found_with_invalid_id( + client: FlaskClient, mocker: MockFixture, valid_header: dict +): from time_tracker_api.customers.customers_namespace import customer_dao from werkzeug.exceptions import NotFound invalid_id = fake.random_int(1, 9999) - repository_remove_mock = mocker.patch.object(customer_dao.repository, - 'delete', - side_effect=NotFound) + repository_remove_mock = mocker.patch.object( + customer_dao.repository, 'partial_update', side_effect=NotFound + ) - response = client.delete("/customers/%s" % invalid_id, - headers=valid_header, - follow_redirects=True) + response = client.delete( + "/customers/%s" % invalid_id, + headers=valid_header, + follow_redirects=True, + ) assert HTTPStatus.NOT_FOUND == response.status_code - repository_remove_mock.assert_called_once_with(str(invalid_id), ANY) + repository_remove_mock.assert_called_once_with( + str(invalid_id), {'status': 'inactive'}, ANY + ) -def test_delete_customer_should_return_422_for_invalid_id_format(client: FlaskClient, - mocker: MockFixture, - tenant_id: str, - valid_header: dict): +def test_delete_customer_should_return_422_for_invalid_id_format( + client: FlaskClient, + mocker: MockFixture, + tenant_id: str, + valid_header: dict, +): from time_tracker_api.customers.customers_namespace import customer_dao from werkzeug.exceptions import UnprocessableEntity invalid_id = fake.company() - repository_remove_mock = mocker.patch.object(customer_dao.repository, - 'delete', - side_effect=UnprocessableEntity) + repository_remove_mock = mocker.patch.object( + customer_dao.repository, + 'partial_update', + side_effect=UnprocessableEntity, + ) - response = client.delete("/customers/%s" % invalid_id, - headers=valid_header, - follow_redirects=True) + response = client.delete( + "/customers/%s" % invalid_id, + headers=valid_header, + follow_redirects=True, + ) assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code - repository_remove_mock.assert_called_once_with(str(invalid_id), ANY) + repository_remove_mock.assert_called_once_with( + str(invalid_id), {'status': 'inactive'}, ANY + ) + + +def test_list_all_active_customers( + client: FlaskClient, mocker: MockFixture, valid_header: dict +): + from time_tracker_api.customers.customers_namespace import customer_dao + + repository_find_all_mock = mocker.patch.object( + customer_dao.repository, 'find_all', return_value=[] + ) + + response = client.get( + "/customers", headers=valid_header, follow_redirects=True + ) + + assert HTTPStatus.OK == response.status_code + json_data = json.loads(response.data) + assert [] == json_data + + repository_find_all_mock.assert_called_once_with(ANY, conditions={}) + + +# def test_list_only_active_customers( +# client: FlaskClient, mocker: MockFixture, valid_header: dict +# ): +# from time_tracker_api.customers.customers_namespace import customer_dao + +# repository_find_all_mock = mocker.patch.object( +# customer_dao.repository, 'find_all', return_value=[] +# ) + +# response = client.get( +# "/customers?status=active", +# headers=valid_header, +# follow_redirects=True, +# ) diff --git a/time_tracker_api/activities/activities_model.py b/time_tracker_api/activities/activities_model.py index 714bb28c..a80bc384 100644 --- a/time_tracker_api/activities/activities_model.py +++ b/time_tracker_api/activities/activities_model.py @@ -10,7 +10,10 @@ from time_tracker_api.database import CRUDDao, APICosmosDBDao from typing import List, Callable from commons.data_access_layer.database import EventContext -from utils.repository import convert_list_to_tuple_string, create_sql_in_condition +from utils.repository import ( + convert_list_to_tuple_string, + create_sql_in_condition, +) class ActivityDao(CRUDDao): @@ -62,7 +65,7 @@ def find_all_with_id_in_list( activity_ids: List[str], visible_only=True, mapper: Callable = None, - ): + ): visibility = self.create_sql_condition_for_visibility(visible_only) query_str = """ SELECT * FROM c diff --git a/time_tracker_api/customers/customers_model.py b/time_tracker_api/customers/customers_model.py index d6a30e9c..d780a127 100644 --- a/time_tracker_api/customers/customers_model.py +++ b/time_tracker_api/customers/customers_model.py @@ -2,7 +2,11 @@ from azure.cosmos import PartitionKey -from commons.data_access_layer.cosmos_db import CosmosDBModel, CosmosDBRepository, CosmosDBDao +from commons.data_access_layer.cosmos_db import ( + CosmosDBModel, + CosmosDBRepository, + CosmosDBDao, +) from time_tracker_api.database import CRUDDao, APICosmosDBDao @@ -17,7 +21,7 @@ class CustomerDao(CRUDDao): 'uniqueKeys': [ {'paths': ['/name', '/deleted']}, ] - } + }, } @@ -28,6 +32,7 @@ class CustomerCosmosDBModel(CosmosDBModel): description: str deleted: str tenant_id: str + status: str def __init__(self, data): super(CustomerCosmosDBModel, self).__init__(data) # pragma: no cover @@ -40,8 +45,9 @@ def __str___(self): def create_dao() -> CustomerDao: - repository = CosmosDBRepository.from_definition(container_definition, - mapper=CustomerCosmosDBModel) + repository = CosmosDBRepository.from_definition( + container_definition, mapper=CustomerCosmosDBModel + ) class CustomerCosmosDBDao(APICosmosDBDao, CustomerDao): def __init__(self): diff --git a/time_tracker_api/customers/customers_namespace.py b/time_tracker_api/customers/customers_namespace.py index 81550438..584616a4 100644 --- a/time_tracker_api/customers/customers_namespace.py +++ b/time_tracker_api/customers/customers_namespace.py @@ -2,55 +2,87 @@ from flask_restplus import Resource, fields from flask_restplus._http import HTTPStatus -from time_tracker_api.api import common_fields, api, remove_required_constraint, NullableString +from time_tracker_api.api import ( + common_fields, + api, + remove_required_constraint, + NullableString, +) from time_tracker_api.customers.customers_model import create_dao faker = Faker() -ns = api.namespace('customers', description='Namespace of the API for customers') +ns = api.namespace( + 'customers', description='Namespace of the API for customers' +) # Customer Model -customer_input = ns.model('CustomerInput', { - 'name': fields.String( - title='Name', - required=True, - max_length=50, - description='Name of the customer', - example=faker.company(), - ), - 'description': NullableString( - title='Description', - required=False, - max_length=250, - description='Description about the customer', - example=faker.paragraph(), - ), -}) +customer_input = ns.model( + 'CustomerInput', + { + 'name': fields.String( + title='Name', + required=True, + max_length=50, + description='Name of the customer', + example=faker.company(), + ), + 'description': NullableString( + title='Description', + required=False, + max_length=250, + description='Description about the customer', + example=faker.paragraph(), + ), + 'status': fields.String( + required=False, + title='Status', + description='Status active or inactive activities', + example=Faker().words( + 2, + [ + 'active', + 'inactive', + ], + unique=True, + ), + ), + }, +) customer_response_fields = {} customer_response_fields.update(common_fields) -customer = ns.inherit( - 'Customer', - customer_input, - customer_response_fields -) +customer = ns.inherit('Customer', customer_input, customer_response_fields) customer_dao = create_dao() +list_customers_attribs_parser = ns.parser() +list_customers_attribs_parser.add_argument( + 'status', + required=False, + store_missing=False, + help="(Filter) Permits to get a list of customers actives or inactives", + location='args', +) + @ns.route('') class Customers(Resource): @ns.doc('list_customers') @ns.marshal_list_with(customer) + @ns.expect(list_customers_attribs_parser) def get(self): """List all customers""" - return customer_dao.get_all() + conditions = list_customers_attribs_parser.parse_args() + return customer_dao.get_all(conditions=conditions) @ns.doc('create_customer') @ns.response(HTTPStatus.CONFLICT, 'This customer already exists') - @ns.response(HTTPStatus.BAD_REQUEST, 'Invalid format or structure ' - 'of the attributes of the customer') + @ns.response( + HTTPStatus.BAD_REQUEST, + 'Invalid format or structure ' 'of the attributes of the customer', + ) @ns.expect(customer_input) @ns.marshal_with(customer, code=HTTPStatus.CREATED) def post(self): @@ -64,16 +96,22 @@ def post(self): @ns.param('id', 'The customer identifier') class Customer(Resource): @ns.doc('get_customer') - @ns.response(HTTPStatus.UNPROCESSABLE_ENTITY, 'The id has an invalid format') + @ns.response( + HTTPStatus.UNPROCESSABLE_ENTITY, 'The id has an invalid format' + ) @ns.marshal_with(customer) def get(self, id): """Get a customer""" return customer_dao.get(id) @ns.doc('update_customer') - @ns.response(HTTPStatus.BAD_REQUEST, 'Invalid format or structure ' - 'of the attributes of the customer') - @ns.response(HTTPStatus.CONFLICT, 'A customer already exists with this new data') + @ns.response( + HTTPStatus.BAD_REQUEST, + 'Invalid format or structure ' 'of the attributes of the customer', + ) + @ns.response( + HTTPStatus.CONFLICT, 'A customer already exists with this new data' + ) @ns.expect(remove_required_constraint(customer_input)) @ns.marshal_with(customer) def put(self, id): @@ -84,5 +122,5 @@ def put(self, id): @ns.response(HTTPStatus.NO_CONTENT, 'Customer successfully deleted') def delete(self, id): """Delete a customer""" - customer_dao.delete(id) + customer_dao.update(id, {'status': 'inactive'}) return None, HTTPStatus.NO_CONTENT