From b8bbd8e145c4bdc6a5dfb495a381c29130101e8f Mon Sep 17 00:00:00 2001 From: semantic-release Date: Tue, 21 Apr 2020 18:13:32 +0000 Subject: [PATCH 01/13] 0.4.0 Automatically generated by python-semantic-release --- time_tracker_api/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index 73e3bb4f..abeeedbf 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.3.2' +__version__ = '0.4.0' From d315d5bded6b39b72468bcaa8500323f89e8ebbe Mon Sep 17 00:00:00 2001 From: roberto Date: Tue, 21 Apr 2020 13:50:30 -0500 Subject: [PATCH 02/13] fix: Add ProxyFix to serve swagger.json over HTTPs --- time_tracker_api/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/time_tracker_api/__init__.py b/time_tracker_api/__init__.py index 6e633c7d..7a33a4db 100644 --- a/time_tracker_api/__init__.py +++ b/time_tracker_api/__init__.py @@ -46,6 +46,8 @@ def init_app(app: Flask): app.logger.setLevel(logging.INFO) add_debug_toolbar(app) + add_werkzeug_proxy_fix(app) + cors_origins = app.config.get('CORS_ORIGINS') if cors_origins: enable_cors(app, cors_origins) @@ -74,3 +76,9 @@ def enable_cors(app: Flask, cors_origins: str): cors_origins_list = cors_origins.split(",") CORS(app, resources={r"/*": {"origins": cors_origins_list}}) app.logger.info("Set CORS access to [%s]" % cors_origins) + + +def add_werkzeug_proxy_fix(app: Flask): + from werkzeug.middleware.proxy_fix import ProxyFix + app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) + app.logger.info("Add ProxyFix to serve swagger.json over https.") From 41a73da7d251d6047d60eca0b580c96a7c75ddbc Mon Sep 17 00:00:00 2001 From: semantic-release Date: Tue, 21 Apr 2020 19:06:07 +0000 Subject: [PATCH 03/13] 0.4.1 Automatically generated by python-semantic-release --- time_tracker_api/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index abeeedbf..f0ede3d3 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.4.0' +__version__ = '0.4.1' From 1a41ed7077c121c595ab29f2b6ff2fcdf8a3618f Mon Sep 17 00:00:00 2001 From: EliuX Date: Tue, 21 Apr 2020 20:00:09 -0500 Subject: [PATCH 04/13] feat: Ack JWT claims for authentication #94 --- requirements/time_tracker_api/prod.txt | 7 +- tests/conftest.py | 52 +++++++++- .../activities/activities_namespace_test.py | 25 +++-- tests/time_tracker_api/security_test.py | 34 +++++++ time_tracker_api/api.py | 5 +- time_tracker_api/config.py | 4 +- time_tracker_api/security.py | 97 +++++++++++++++---- 7 files changed, 194 insertions(+), 30 deletions(-) create mode 100644 tests/time_tracker_api/security_test.py diff --git a/requirements/time_tracker_api/prod.txt b/requirements/time_tracker_api/prod.txt index d72284f5..344bc4bb 100644 --- a/requirements/time_tracker_api/prod.txt +++ b/requirements/time_tracker_api/prod.txt @@ -26,8 +26,11 @@ Flask-Script==2.0.6 #Semantic versioning python-semantic-release==5.2.0 -# The Debug Toolbar +#The Debug Toolbar Flask-DebugToolbar==0.11.0 #CORS -flask-cors==3.0.8 \ No newline at end of file +flask-cors==3.0.8 + +#JWT +PyJWT==1.7.1 diff --git a/tests/conftest.py b/tests/conftest.py index 97b80b65..21de5386 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,50 @@ +from datetime import datetime, timedelta + +import jwt import pytest from faker import Faker -from flask import Flask +from flask import Flask, url_for from flask.testing import FlaskClient from commons.data_access_layer.cosmos_db import CosmosDBRepository, datetime_str, current_datetime from time_tracker_api import create_app +from time_tracker_api.security import get_or_generate_dev_secret_key from time_tracker_api.time_entries.time_entries_model import TimeEntryCosmosDBRepository fake = Faker() Faker.seed() +TEST_USER = { + "name": "testuser@ioet.com", + "password": "secret" +} + + +class User: + def __init__(self, username, password): + self.username = username + self.password = password + + +class AuthActions: + """Auth actions container in tests""" + + def __init__(self, app, client): + self._app = app + self._client = client + + # def login(self, username=TEST_USER["name"], + # password=TEST_USER["password"]): + # login_url = url_for("security.login", self._app) + # return open_with_basic_auth(self._client, + # login_url, + # username, + # password) + # + # def logout(self): + # return self._client.get(url_for("security.logout", self._app), + # follow_redirects=True) + @pytest.fixture(scope='session') def app() -> Flask: @@ -148,3 +183,18 @@ def running_time_entry(time_entry_repository: TimeEntryCosmosDBRepository, time_entry_repository.delete(id=created_time_entry.id, partition_key_value=tenant_id) + + +@pytest.fixture(scope="session") +def valid_jwt(app: Flask) -> str: + expiration_time = datetime.utcnow() + timedelta(seconds=3600) + return jwt.encode({ + "iss": "https://securityioet.b2clogin.com/%s/v2.0/" % fake.uuid4(), + "oid": fake.uuid4(), + 'exp': expiration_time + }, key=get_or_generate_dev_secret_key()).decode("UTF-8") + + +@pytest.fixture(scope="session") +def valid_header(valid_jwt: str) -> dict: + return {'Authorization': "Bearer %s" % valid_jwt} diff --git a/tests/time_tracker_api/activities/activities_namespace_test.py b/tests/time_tracker_api/activities/activities_namespace_test.py index 1b69fd74..90f3f1d6 100644 --- a/tests/time_tracker_api/activities/activities_namespace_test.py +++ b/tests/time_tracker_api/activities/activities_namespace_test.py @@ -19,13 +19,18 @@ }).update(valid_activity_data) -def test_create_activity_should_succeed_with_valid_request(client: FlaskClient, mocker: MockFixture): +def test_create_activity_should_succeed_with_valid_request(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): from time_tracker_api.activities.activities_namespace import activity_dao repository_create_mock = mocker.patch.object(activity_dao.repository, 'create', return_value=fake_activity) - response = client.post("/activities", json=valid_activity_data, follow_redirects=True) + response = client.post("/activities", + headers=valid_header, + json=valid_activity_data, + follow_redirects=True) assert HTTPStatus.CREATED == response.status_code repository_create_mock.assert_called_once() @@ -57,7 +62,9 @@ def test_list_all_activities(client: FlaskClient, mocker: MockFixture): repository_find_all_mock.assert_called_once() -def test_get_activity_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture): +def test_get_activity_should_succeed_with_valid_id(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): from time_tracker_api.activities.activities_namespace import activity_dao valid_id = fake.random_int(1, 9999) @@ -66,7 +73,9 @@ def test_get_activity_should_succeed_with_valid_id(client: FlaskClient, mocker: 'find', return_value=fake_activity) - response = client.get("/activities/%s" % valid_id, follow_redirects=True) + response = client.get("/activities/%s" % valid_id, + headers=valid_header, + follow_redirects=True) assert HTTPStatus.OK == response.status_code fake_activity == json.loads(response.data) @@ -74,7 +83,9 @@ def test_get_activity_should_succeed_with_valid_id(client: FlaskClient, mocker: partition_key_value=current_user_tenant_id()) -def test_get_activity_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture): +def test_get_activity_should_return_not_found_with_invalid_id(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): from time_tracker_api.activities.activities_namespace import activity_dao from werkzeug.exceptions import NotFound @@ -84,7 +95,9 @@ def test_get_activity_should_return_not_found_with_invalid_id(client: FlaskClien 'find', side_effect=NotFound) - response = client.get("/activities/%s" % invalid_id, follow_redirects=True) + response = client.get("/activities/%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), diff --git a/tests/time_tracker_api/security_test.py b/tests/time_tracker_api/security_test.py new file mode 100644 index 00000000..54ffe5e8 --- /dev/null +++ b/tests/time_tracker_api/security_test.py @@ -0,0 +1,34 @@ +from time_tracker_api.security import parse_jwt, parse_tenant_id_from_iss_claim + + +def test_parse_jwt_with_valid_input(valid_jwt: str): + result = parse_jwt("Bearer %s" % valid_jwt) + + assert result is not None + assert type(result) is dict + + +def test_parse_jwt_with_invalid_input(): + result = parse_jwt("whetever") + + assert result is None + + +def test_parse_tenant_id_from_iss_claim_with_valid_input(): + valid_iss_claim = "https://securityioet.b2clogin.com/b21c4e98-c4bf-420f-9d76-e51c2515c7a4/v2.0/" + + result = parse_tenant_id_from_iss_claim(valid_iss_claim) + + assert result is not None + assert type(result) is str + assert result == "b21c4e98-c4bf-420f-9d76-e51c2515c7a4" + + +def test_parse_tenant_id_from_iss_claim_with_invalid_input(): + invalid_iss_claim1 = "https://securityioet.b2clogin.com/whatever/v2.0/" + invalid_iss_claim2 = "" + + result1 = parse_tenant_id_from_iss_claim(invalid_iss_claim1) + result2 = parse_tenant_id_from_iss_claim(invalid_iss_claim2) + + assert result1 == result2 == None diff --git a/time_tracker_api/api.py b/time_tracker_api/api.py index 080dea08..b269136b 100644 --- a/time_tracker_api/api.py +++ b/time_tracker_api/api.py @@ -5,6 +5,7 @@ from flask_restplus._http import HTTPStatus from commons.data_access_layer.cosmos_db import CustomError +from time_tracker_api import security from time_tracker_api.version import __version__ faker = Faker() @@ -12,7 +13,9 @@ api = Api( version=__version__, title="TimeTracker API", - description="API for the TimeTracker project" + description="API for the TimeTracker project", + authorizations=security.authorizations, + security="TimeTracker JWT", ) # For matching UUIDs diff --git a/time_tracker_api/config.py b/time_tracker_api/config.py index c91f50b0..e31d9ab2 100644 --- a/time_tracker_api/config.py +++ b/time_tracker_api/config.py @@ -1,12 +1,12 @@ import os -from time_tracker_api.security import generate_dev_secret_key +from time_tracker_api.security import get_or_generate_dev_secret_key DISABLE_STR_VALUES = ("false", "0", "disabled") class Config: - SECRET_KEY = generate_dev_secret_key() + SECRET_KEY = get_or_generate_dev_secret_key() SQL_DATABASE_URI = os.environ.get('SQL_DATABASE_URI') PROPAGATE_EXCEPTIONS = True RESTPLUS_VALIDATE = True diff --git a/time_tracker_api/security.py b/time_tracker_api/security.py index 684f1dfc..d0c382ec 100644 --- a/time_tracker_api/security.py +++ b/time_tracker_api/security.py @@ -2,36 +2,97 @@ This is where we handle everything regarding to authorization and authentication. Also stores helper functions related to it. """ +import re + +import jwt from faker import Faker +from flask import request +from flask_restplus import abort +from flask_restplus._http import HTTPStatus +from jwt import DecodeError, ExpiredSignatureError fake = Faker() dev_secret_key: str = None +authorizations = { + "TimeTracker JWT": { + 'type': 'apiKey', + 'in': 'header', + 'name': 'Authorization', + 'description': "Specify in the value **'Bearer <JWT>'**, where JWT is the token", + } +} + +iss_claim_pattern = re.compile( + r"securityioet.b2clogin.com/(?P[0-9a-f]{8}\-[0-9a-f]{4}\-4[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12})") + def current_user_id() -> str: - """ - Returns the id of the authenticated user in - Azure Active Directory - """ - return 'anonymous' + oid_claim = get_token_json().get("oid") + if oid_claim is None: + abort(message='The claim "oid" is missing in the JWT', code=HTTPStatus.UNAUTHORIZED) + + return oid_claim def current_user_tenant_id() -> str: - # TODO Get this from the JWT - return "ioet" + iss_claim = get_token_json().get("iss") + if iss_claim is None: + abort(message='The claim "iss" is missing in the JWT', code=HTTPStatus.UNAUTHORIZED) + + tenant_id = parse_tenant_id_from_iss_claim(iss_claim) + if tenant_id is None: + abort(message='The format of the claim "iss" cannot be understood. ' + 'Please contact the development team.', + code=HTTPStatus.UNAUTHORIZED) + return tenant_id -def generate_dev_secret_key(): - from time_tracker_api import flask_app as app - """ - Generates a security key for development purposes - :return: str - """ + +def get_or_generate_dev_secret_key(): global dev_secret_key - dev_secret_key = fake.password(length=16, special_chars=True, digits=True, upper_case=True, lower_case=True) - if app.config.get("FLASK_DEBUG", False): # pragma: no cover - print('*********************************************************') - print("The generated secret is \"%s\"" % dev_secret_key) - print('*********************************************************') + if dev_secret_key is None: + from time_tracker_api import flask_app as app + """ + Generates a security key for development purposes + :return: str + """ + dev_secret_key = fake.password(length=16, special_chars=True, digits=True, upper_case=True, lower_case=True) + if app.config.get("FLASK_DEBUG", False): # pragma: no cover + print('*********************************************************') + print("The generated secret is \"%s\"" % dev_secret_key) + print('*********************************************************') return dev_secret_key + + +def parse_jwt(authentication_header_content): + if authentication_header_content is not None: + parsed_content = authentication_header_content.split("Bearer ") + + if len(parsed_content) > 1: + return jwt.decode(parsed_content[1], verify=False) + + return None + + +def get_authorization_jwt(): + auth_header = request.headers.get('Authorization') + return parse_jwt(auth_header) + + +def get_token_json(): + try: + return get_authorization_jwt() + except DecodeError: + abort(message='Malformed token', code=HTTPStatus.UNAUTHORIZED) + except ExpiredSignatureError: + abort(message='Expired token', code=HTTPStatus.UNAUTHORIZED) + + +def parse_tenant_id_from_iss_claim(iss_claim: str) -> str: + m = iss_claim_pattern.search(iss_claim) + if m is not None: + return m.group('tenant_id') + + return None From 7efc921ce662c4d7b2d264e2837c5acb5fe5690c Mon Sep 17 00:00:00 2001 From: EliuX Date: Wed, 22 Apr 2020 12:01:37 -0500 Subject: [PATCH 05/13] fix: Close #94 Fix tests to support authentication --- commons/data_access_layer/database.py | 1 - commons/data_access_layer/sql.py | 12 -- tests/commons/data_access_layer/sql_test.py | 7 - tests/conftest.py | 47 +---- .../activities/activities_namespace_test.py | 99 +++++++--- .../customers/customers_namespace_test.py | 125 ++++++++---- .../project_types_namespace_test.py | 124 ++++++++---- .../projects/projects_namespace_test.py | 121 ++++++++---- .../time_entries_namespace_test.py | 182 +++++++++++++----- 9 files changed, 480 insertions(+), 238 deletions(-) diff --git a/commons/data_access_layer/database.py b/commons/data_access_layer/database.py index 077780cf..27bee0ca 100644 --- a/commons/data_access_layer/database.py +++ b/commons/data_access_layer/database.py @@ -36,7 +36,6 @@ def delete(self, id): def init_app(app: Flask) -> None: - init_sql(app) # TODO Delete after the migration to Cosmos DB has finished. init_cosmos_db(app) diff --git a/commons/data_access_layer/sql.py b/commons/data_access_layer/sql.py index c1afb0e2..eb356620 100644 --- a/commons/data_access_layer/sql.py +++ b/commons/data_access_layer/sql.py @@ -4,10 +4,8 @@ from flask_sqlalchemy import SQLAlchemy from commons.data_access_layer.database import CRUDDao, ID_MAX_LENGTH -from time_tracker_api.security import current_user_id db: SQLAlchemy = None -AuditedSQLModel = None def handle_commit_issues(f): @@ -25,16 +23,6 @@ def init_app(app: Flask) -> None: global db db = SQLAlchemy(app) - global AuditedSQLModel - - class AuditedSQLModelClass(): - created_at = db.Column(db.DateTime, server_default=db.func.now()) - updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) - created_by = db.Column(db.String(ID_MAX_LENGTH), default=current_user_id) - updated_by = db.Column(db.String(ID_MAX_LENGTH), onupdate=current_user_id) - - AuditedSQLModel = AuditedSQLModelClass - class SQLRepository(): def __init__(self, model_type: type): diff --git a/tests/commons/data_access_layer/sql_test.py b/tests/commons/data_access_layer/sql_test.py index 48f220ff..26f992b8 100644 --- a/tests/commons/data_access_layer/sql_test.py +++ b/tests/commons/data_access_layer/sql_test.py @@ -16,10 +16,6 @@ def test_create(sql_repository): assert result is not None assert result.id is not None - assert result.created_at is not None - assert result.created_by is not None - assert result.updated_at is None - assert result.updated_by is None existing_elements_registry.append(result) @@ -43,9 +39,6 @@ def test_update(sql_repository): assert updated_element.id == existing_element.id assert updated_element.name == "Jon Snow" assert updated_element.age == 34 - assert updated_element.updated_at is not None - assert updated_element.updated_at > updated_element.created_at - assert updated_element.updated_by is not None def test_find_all(sql_repository): diff --git a/tests/conftest.py b/tests/conftest.py index 21de5386..5fad738c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ from flask.testing import FlaskClient from commons.data_access_layer.cosmos_db import CosmosDBRepository, datetime_str, current_datetime +from commons.data_access_layer.database import init_sql from time_tracker_api import create_app from time_tracker_api.security import get_or_generate_dev_secret_key from time_tracker_api.time_entries.time_entries_model import TimeEntryCosmosDBRepository @@ -14,37 +15,6 @@ fake = Faker() Faker.seed() -TEST_USER = { - "name": "testuser@ioet.com", - "password": "secret" -} - - -class User: - def __init__(self, username, password): - self.username = username - self.password = password - - -class AuthActions: - """Auth actions container in tests""" - - def __init__(self, app, client): - self._app = app - self._client = client - - # def login(self, username=TEST_USER["name"], - # password=TEST_USER["password"]): - # login_url = url_for("security.login", self._app) - # return open_with_basic_auth(self._client, - # login_url, - # username, - # password) - # - # def logout(self): - # return self._client.get(url_for("security.logout", self._app), - # follow_redirects=True) - @pytest.fixture(scope='session') def app() -> Flask: @@ -58,9 +28,12 @@ def client(app: Flask) -> FlaskClient: @pytest.fixture(scope="module") -def sql_model_class(): - from commons.data_access_layer.sql import db, AuditedSQLModel - class PersonSQLModel(db.Model, AuditedSQLModel): +def sql_model_class(app: Flask): + with app.app_context(): + init_sql(app) + + from commons.data_access_layer.sql import db + class PersonSQLModel(db.Model): __tablename__ = 'test' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80), unique=False, nullable=False) @@ -186,11 +159,11 @@ def running_time_entry(time_entry_repository: TimeEntryCosmosDBRepository, @pytest.fixture(scope="session") -def valid_jwt(app: Flask) -> str: +def valid_jwt(app: Flask, tenant_id: str, owner_id: str) -> str: expiration_time = datetime.utcnow() + timedelta(seconds=3600) return jwt.encode({ - "iss": "https://securityioet.b2clogin.com/%s/v2.0/" % fake.uuid4(), - "oid": fake.uuid4(), + "iss": "https://securityioet.b2clogin.com/%s/v2.0/" % tenant_id, + "oid": owner_id, 'exp': expiration_time }, key=get_or_generate_dev_secret_key()).decode("UTF-8") diff --git a/tests/time_tracker_api/activities/activities_namespace_test.py b/tests/time_tracker_api/activities/activities_namespace_test.py index 90f3f1d6..508e7f88 100644 --- a/tests/time_tracker_api/activities/activities_namespace_test.py +++ b/tests/time_tracker_api/activities/activities_namespace_test.py @@ -4,8 +4,6 @@ from flask_restplus._http import HTTPStatus from pytest_mock import MockFixture -from time_tracker_api.security import current_user_tenant_id - fake = Faker() valid_activity_data = { @@ -36,34 +34,45 @@ def test_create_activity_should_succeed_with_valid_request(client: FlaskClient, repository_create_mock.assert_called_once() -def test_create_activity_should_reject_bad_request(client: FlaskClient, mocker: MockFixture): +def test_create_activity_should_reject_bad_request(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): from time_tracker_api.activities.activities_namespace import activity_dao repository_create_mock = mocker.patch.object(activity_dao.repository, 'create', return_value=fake_activity) - response = client.post("/activities", json=None, follow_redirects=True) + response = client.post("/activities", + 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_activities(client: FlaskClient, mocker: MockFixture): +def test_list_all_activities(client: FlaskClient, + mocker: MockFixture, + tenant_id: str, + valid_header: dict): from time_tracker_api.activities.activities_namespace import activity_dao repository_find_all_mock = mocker.patch.object(activity_dao.repository, 'find_all', return_value=[]) - response = client.get("/activities", follow_redirects=True) + response = client.get("/activities", + 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() + repository_find_all_mock.assert_called_once_with(partition_key_value=tenant_id) def test_get_activity_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture, + tenant_id: str, valid_header: dict): from time_tracker_api.activities.activities_namespace import activity_dao @@ -79,12 +88,12 @@ def test_get_activity_should_succeed_with_valid_id(client: FlaskClient, assert HTTPStatus.OK == response.status_code fake_activity == json.loads(response.data) - repository_find_mock.assert_called_once_with(str(valid_id), - partition_key_value=current_user_tenant_id()) + repository_find_mock.assert_called_once_with(str(valid_id), partition_key_value=tenant_id) def test_get_activity_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture, + tenant_id: str, valid_header: dict): from time_tracker_api.activities.activities_namespace import activity_dao from werkzeug.exceptions import NotFound @@ -101,10 +110,11 @@ def test_get_activity_should_return_not_found_with_invalid_id(client: FlaskClien assert HTTPStatus.NOT_FOUND == response.status_code repository_find_mock.assert_called_once_with(str(invalid_id), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_get_activity_should_return_422_for_invalid_id_format(client: FlaskClient, mocker: MockFixture): +def test_get_activity_should_return_422_for_invalid_id_format(client: FlaskClient, + mocker: MockFixture): from time_tracker_api.activities.activities_namespace import activity_dao from werkzeug.exceptions import UnprocessableEntity @@ -117,41 +127,54 @@ def test_get_activity_should_return_422_for_invalid_id_format(client: FlaskClien response = client.get("/activities/%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()) + repository_find_mock.assert_not_called() -def test_update_activity_should_succeed_with_valid_data(client: FlaskClient, mocker: MockFixture): +def test_update_activity_should_succeed_with_valid_data(client: FlaskClient, + tenant_id: str, + mocker: MockFixture, + valid_header: dict): from time_tracker_api.activities.activities_namespace import activity_dao repository_update_mock = mocker.patch.object(activity_dao.repository, 'partial_update', return_value=fake_activity) - valid_id = fake.random_int(1, 9999) - response = client.put("/activities/%s" % valid_id, json=valid_activity_data, follow_redirects=True) + valid_id = fake.uuid4() + response = client.put("/activities/%s" % valid_id, + headers=valid_header, + json=valid_activity_data, + follow_redirects=True) assert HTTPStatus.OK == response.status_code fake_activity == json.loads(response.data) repository_update_mock.assert_called_once_with(str(valid_id), changes=valid_activity_data, - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_update_activity_should_reject_bad_request(client: FlaskClient, mocker: MockFixture): +def test_update_activity_should_reject_bad_request(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): from time_tracker_api.activities.activities_namespace import activity_dao repository_update_mock = mocker.patch.object(activity_dao.repository, 'partial_update', return_value=fake_activity) valid_id = fake.random_int(1, 9999) - response = client.put("/activities/%s" % valid_id, json=None, follow_redirects=True) + response = client.put("/activities/%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_activity_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture): +def test_update_activity_should_return_not_found_with_invalid_id(client: FlaskClient, + tenant_id: str, + mocker: MockFixture, + valid_header: dict): from time_tracker_api.activities.activities_namespace import activity_dao from werkzeug.exceptions import NotFound @@ -162,16 +185,20 @@ def test_update_activity_should_return_not_found_with_invalid_id(client: FlaskCl side_effect=NotFound) response = client.put("/activities/%s" % invalid_id, + headers=valid_header, json=valid_activity_data, follow_redirects=True) assert HTTPStatus.NOT_FOUND == response.status_code repository_update_mock.assert_called_once_with(str(invalid_id), changes=valid_activity_data, - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_delete_activity_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture): +def test_delete_activity_should_succeed_with_valid_id(client: FlaskClient, + mocker: MockFixture, + tenant_id: str, + valid_header: dict): from time_tracker_api.activities.activities_namespace import activity_dao valid_id = fake.random_int(1, 9999) @@ -180,15 +207,20 @@ def test_delete_activity_should_succeed_with_valid_id(client: FlaskClient, mocke 'delete', return_value=None) - response = client.delete("/activities/%s" % valid_id, follow_redirects=True) + response = client.delete("/activities/%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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_delete_activity_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture): +def test_delete_activity_should_return_not_found_with_invalid_id(client: FlaskClient, + mocker: MockFixture, + tenant_id: str, + valid_header: dict): from time_tracker_api.activities.activities_namespace import activity_dao from werkzeug.exceptions import NotFound @@ -198,14 +230,19 @@ def test_delete_activity_should_return_not_found_with_invalid_id(client: FlaskCl 'delete', side_effect=NotFound) - response = client.delete("/activities/%s" % invalid_id, follow_redirects=True) + response = client.delete("/activities/%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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_delete_activity_should_return_422_for_invalid_id_format(client: FlaskClient, mocker: MockFixture): +def test_delete_activity_should_return_422_for_invalid_id_format(client: FlaskClient, + mocker: MockFixture, + tenant_id: str, + valid_header: dict): from time_tracker_api.activities.activities_namespace import activity_dao from werkzeug.exceptions import UnprocessableEntity @@ -215,8 +252,10 @@ def test_delete_activity_should_return_422_for_invalid_id_format(client: FlaskCl 'delete', side_effect=UnprocessableEntity) - response = client.delete("/activities/%s" % invalid_id, follow_redirects=True) + response = client.delete("/activities/%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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) diff --git a/tests/time_tracker_api/customers/customers_namespace_test.py b/tests/time_tracker_api/customers/customers_namespace_test.py index 707df28a..1777cbe8 100644 --- a/tests/time_tracker_api/customers/customers_namespace_test.py +++ b/tests/time_tracker_api/customers/customers_namespace_test.py @@ -4,8 +4,6 @@ from flask_restplus._http import HTTPStatus from pytest_mock import MockFixture -from time_tracker_api.security import current_user_tenant_id - fake = Faker() valid_customer_data = { @@ -15,41 +13,55 @@ } fake_customer = ({ - "id": fake.random_int(1, 9999) + "id": fake.random_int(1, 9999), }).update(valid_customer_data) -def test_create_customer_should_succeed_with_valid_request(client: FlaskClient, mocker: MockFixture): +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", json=valid_customer_data, follow_redirects=True) + 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): +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", json=None, follow_redirects=True) + 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): +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", follow_redirects=True) + response = client.get("/customers", + headers=valid_header, + follow_redirects=True) assert HTTPStatus.OK == response.status_code json_data = json.loads(response.data) @@ -57,7 +69,10 @@ def test_list_all_customers(client: FlaskClient, mocker: MockFixture): repository_find_all_mock.assert_called_once() -def test_get_customer_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture): +def test_get_customer_should_succeed_with_valid_id(client: FlaskClient, + mocker: MockFixture, + tenant_id: str, + valid_header: dict): from time_tracker_api.customers.customers_namespace import customer_dao valid_id = fake.random_int(1, 9999) @@ -66,15 +81,20 @@ def test_get_customer_should_succeed_with_valid_id(client: FlaskClient, mocker: 'find', return_value=fake_customer) - response = client.get("/customers/%s" % valid_id, 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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_get_customer_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture): +def test_get_customer_should_return_not_found_with_invalid_id(client: FlaskClient, + mocker: MockFixture, + tenant_id: str, + valid_header: dict): from time_tracker_api.customers.customers_namespace import customer_dao from werkzeug.exceptions import NotFound @@ -84,14 +104,19 @@ def test_get_customer_should_return_not_found_with_invalid_id(client: FlaskClien 'find', side_effect=NotFound) - response = client.get("/customers/%s" % invalid_id, 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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_get_customer_should_return_422_for_invalid_id_format(client: FlaskClient, mocker: MockFixture): +def test_get_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 @@ -101,14 +126,19 @@ def test_get_customer_should_return_422_for_invalid_id_format(client: FlaskClien 'find', side_effect=UnprocessableEntity) - response = client.get("/customers/%s" % invalid_id, 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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_update_customer_should_succeed_with_valid_data(client: FlaskClient, mocker: MockFixture): +def test_update_customer_should_succeed_with_valid_data(client: FlaskClient, + mocker: MockFixture, + tenant_id: str, + valid_header: dict): from time_tracker_api.customers.customers_namespace import customer_dao repository_update_mock = mocker.patch.object(customer_dao.repository, @@ -116,29 +146,40 @@ def test_update_customer_should_succeed_with_valid_data(client: FlaskClient, moc return_value=fake_customer) valid_id = fake.random_int(1, 9999) - response = client.put("/customers/%s" % valid_id, 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), changes=valid_customer_data, - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_update_customer_should_reject_bad_request(client: FlaskClient, mocker: MockFixture): +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) valid_id = fake.random_int(1, 9999) - response = client.put("/customers/%s" % valid_id, 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): +def test_update_customer_should_return_not_found_with_invalid_id(client: FlaskClient, + mocker: MockFixture, + tenant_id: str, + valid_header: dict): from time_tracker_api.customers.customers_namespace import customer_dao from werkzeug.exceptions import NotFound @@ -149,16 +190,20 @@ def test_update_customer_should_return_not_found_with_invalid_id(client: FlaskCl side_effect=NotFound) 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), changes=valid_customer_data, - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_delete_customer_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture): +def test_delete_customer_should_succeed_with_valid_id(client: FlaskClient, + mocker: MockFixture, + tenant_id: str, + valid_header: dict): from time_tracker_api.customers.customers_namespace import customer_dao valid_id = fake.random_int(1, 9999) @@ -167,15 +212,20 @@ def test_delete_customer_should_succeed_with_valid_id(client: FlaskClient, mocke 'delete', return_value=None) - response = client.delete("/customers/%s" % valid_id, 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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_delete_customer_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture): +def test_delete_customer_should_return_not_found_with_invalid_id(client: FlaskClient, + mocker: MockFixture, + tenant_id: str, + valid_header: dict): from time_tracker_api.customers.customers_namespace import customer_dao from werkzeug.exceptions import NotFound @@ -185,14 +235,19 @@ def test_delete_customer_should_return_not_found_with_invalid_id(client: FlaskCl 'delete', side_effect=NotFound) - response = client.delete("/customers/%s" % invalid_id, 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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_delete_customer_should_return_422_for_invalid_id_format(client: FlaskClient, mocker: MockFixture): +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 @@ -202,8 +257,10 @@ def test_delete_customer_should_return_422_for_invalid_id_format(client: FlaskCl 'delete', side_effect=UnprocessableEntity) - response = client.delete("/customers/%s" % invalid_id, 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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) diff --git a/tests/time_tracker_api/project_types/project_types_namespace_test.py b/tests/time_tracker_api/project_types/project_types_namespace_test.py index 613b0cdc..3d0b3135 100644 --- a/tests/time_tracker_api/project_types/project_types_namespace_test.py +++ b/tests/time_tracker_api/project_types/project_types_namespace_test.py @@ -4,8 +4,6 @@ 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 = { @@ -21,19 +19,26 @@ }).update(valid_project_type_data) -def test_create_project_type_should_succeed_with_valid_request(client: FlaskClient, mocker: MockFixture): +def test_create_project_type_should_succeed_with_valid_request(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): 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) + response = client.post("/project-types", + headers=valid_header, + 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): +def test_create_project_type_should_reject_bad_request(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): 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({ @@ -43,59 +48,77 @@ def test_create_project_type_should_reject_bad_request(client: FlaskClient, mock 'create', return_value=fake_project_type) - response = client.post("/project-types", json=invalid_project_type_data, follow_redirects=True) + response = client.post("/project-types", + headers=valid_header, + 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): +def test_list_all_project_types(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): 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) + response = client.get("/project-types", + headers=valid_header, + 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): +def test_get_project_should_succeed_with_valid_id(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): 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) + response = client.get("/project-types/%s" % valid_id, + headers=valid_header, + 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()) + partition_key_value=tenant_id) -def test_get_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture): +def test_get_project_should_return_not_found_with_invalid_id(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): 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) + invalid_id = str(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) + response = client.get("/project-types/%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), - partition_key_value=current_user_tenant_id()) + repository_find_mock.assert_called_once_with(invalid_id, partition_key_value=tenant_id) def test_get_project_should_response_with_unprocessable_entity_for_invalid_id_format(client: FlaskClient, - mocker: MockFixture): + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.project_types.project_types_namespace import project_type_dao from werkzeug.exceptions import UnprocessableEntity @@ -105,14 +128,19 @@ def test_get_project_should_response_with_unprocessable_entity_for_invalid_id_fo 'find', side_effect=UnprocessableEntity) - response = client.get("/project-types/%s" % invalid_id, follow_redirects=True) + response = client.get("/project-types/%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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_update_project_should_succeed_with_valid_data(client: FlaskClient, mocker: MockFixture): +def test_update_project_should_succeed_with_valid_data(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.project_types.project_types_namespace import project_type_dao repository_update_mock = mocker.patch.object(project_type_dao.repository, @@ -120,16 +148,21 @@ def test_update_project_should_succeed_with_valid_data(client: FlaskClient, mock 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) + response = client.put("/project-types/%s" % valid_id, + headers=valid_header, + 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()) + partition_key_value=tenant_id) -def test_update_project_should_reject_bad_request(client: FlaskClient, mocker: MockFixture): +def test_update_project_should_reject_bad_request(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): 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({ @@ -140,13 +173,19 @@ def test_update_project_should_reject_bad_request(client: FlaskClient, mocker: M 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) + response = client.put("/project-types/%s" % valid_id, + headers=valid_header, + 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): +def test_update_project_should_return_not_found_with_invalid_id(client: FlaskClient, + mocker: MockFixture, + tenant_id: str, + valid_header: dict): from time_tracker_api.project_types.project_types_namespace import project_type_dao from werkzeug.exceptions import NotFound @@ -157,16 +196,20 @@ def test_update_project_should_return_not_found_with_invalid_id(client: FlaskCli side_effect=NotFound) response = client.put("/project-types/%s" % invalid_id, + headers=valid_header, 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()) + partition_key_value=tenant_id) -def test_delete_project_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture): +def test_delete_project_should_succeed_with_valid_id(client: FlaskClient, + mocker: MockFixture, + tenant_id: str, + valid_header: dict): from time_tracker_api.project_types.project_types_namespace import project_type_dao valid_id = fake.random_int(1, 9999) @@ -175,15 +218,20 @@ def test_delete_project_should_succeed_with_valid_id(client: FlaskClient, mocker 'delete', return_value=None) - response = client.delete("/project-types/%s" % valid_id, follow_redirects=True) + response = client.delete("/project-types/%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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_delete_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture): +def test_delete_project_should_return_not_found_with_invalid_id(client: FlaskClient, + mocker: MockFixture, + tenant_id: str, + valid_header: dict): from time_tracker_api.project_types.project_types_namespace import project_type_dao from werkzeug.exceptions import NotFound @@ -193,15 +241,19 @@ def test_delete_project_should_return_not_found_with_invalid_id(client: FlaskCli 'delete', side_effect=NotFound) - response = client.delete("/project-types/%s" % invalid_id, follow_redirects=True) + response = client.delete("/project-types/%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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) def test_delete_project_should_return_unprocessable_entity_for_invalid_id_format(client: FlaskClient, - mocker: MockFixture): + mocker: MockFixture, + tenant_id: str, + valid_header: dict): from time_tracker_api.project_types.project_types_namespace import project_type_dao from werkzeug.exceptions import UnprocessableEntity @@ -211,8 +263,10 @@ def test_delete_project_should_return_unprocessable_entity_for_invalid_id_format 'delete', side_effect=UnprocessableEntity) - response = client.delete("/project-types/%s" % invalid_id, follow_redirects=True) + response = client.delete("/project-types/%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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) diff --git a/tests/time_tracker_api/projects/projects_namespace_test.py b/tests/time_tracker_api/projects/projects_namespace_test.py index e8707dc3..6c7ff5eb 100644 --- a/tests/time_tracker_api/projects/projects_namespace_test.py +++ b/tests/time_tracker_api/projects/projects_namespace_test.py @@ -4,8 +4,6 @@ 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_data = { @@ -20,19 +18,26 @@ }).update(valid_project_data) -def test_create_project_should_succeed_with_valid_request(client: FlaskClient, mocker: MockFixture): +def test_create_project_should_succeed_with_valid_request(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): from time_tracker_api.projects.projects_namespace import project_dao repository_create_mock = mocker.patch.object(project_dao.repository, 'create', return_value=fake_project) - response = client.post("/projects", json=valid_project_data, follow_redirects=True) + response = client.post("/projects", + headers=valid_header, + json=valid_project_data, + follow_redirects=True) assert HTTPStatus.CREATED == response.status_code repository_create_mock.assert_called_once() -def test_create_project_should_reject_bad_request(client: FlaskClient, mocker: MockFixture): +def test_create_project_should_reject_bad_request(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): from time_tracker_api.projects.projects_namespace import project_dao invalid_project_data = valid_project_data.copy() invalid_project_data.update({ @@ -42,41 +47,56 @@ def test_create_project_should_reject_bad_request(client: FlaskClient, mocker: M 'create', return_value=fake_project) - response = client.post("/projects", json=invalid_project_data, follow_redirects=True) + response = client.post("/projects", + headers=valid_header, + json=invalid_project_data, + follow_redirects=True) assert HTTPStatus.BAD_REQUEST == response.status_code repository_create_mock.assert_not_called() -def test_list_all_projects(client: FlaskClient, mocker: MockFixture): +def test_list_all_projects(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): from time_tracker_api.projects.projects_namespace import project_dao repository_find_all_mock = mocker.patch.object(project_dao.repository, 'find_all', return_value=[]) - response = client.get("/projects", follow_redirects=True) + response = client.get("/projects", + headers=valid_header, + 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): +def test_get_project_should_succeed_with_valid_id(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.projects.projects_namespace import project_dao valid_id = fake.random_int(1, 9999) repository_find_mock = mocker.patch.object(project_dao.repository, 'find', return_value=fake_project) - response = client.get("/projects/%s" % valid_id, follow_redirects=True) + response = client.get("/projects/%s" % valid_id, + headers=valid_header, + follow_redirects=True) assert HTTPStatus.OK == response.status_code fake_project == json.loads(response.data) repository_find_mock.assert_called_once_with(str(valid_id), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_get_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture): +def test_get_project_should_return_not_found_with_invalid_id(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.projects.projects_namespace import project_dao from werkzeug.exceptions import NotFound @@ -86,15 +106,19 @@ def test_get_project_should_return_not_found_with_invalid_id(client: FlaskClient 'find', side_effect=NotFound) - response = client.get("/projects/%s" % invalid_id, follow_redirects=True) + response = client.get("/projects/%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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) def test_get_project_should_response_with_unprocessable_entity_for_invalid_id_format(client: FlaskClient, - mocker: MockFixture): + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.projects.projects_namespace import project_dao from werkzeug.exceptions import UnprocessableEntity @@ -104,14 +128,19 @@ def test_get_project_should_response_with_unprocessable_entity_for_invalid_id_fo 'find', side_effect=UnprocessableEntity) - response = client.get("/projects/%s" % invalid_id, follow_redirects=True) + response = client.get("/projects/%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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_update_project_should_succeed_with_valid_data(client: FlaskClient, mocker: MockFixture): +def test_update_project_should_succeed_with_valid_data(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.projects.projects_namespace import project_dao repository_update_mock = mocker.patch.object(project_dao.repository, @@ -119,16 +148,21 @@ def test_update_project_should_succeed_with_valid_data(client: FlaskClient, mock return_value=fake_project) valid_id = fake.random_int(1, 9999) - response = client.put("/projects/%s" % valid_id, json=valid_project_data, follow_redirects=True) + response = client.put("/projects/%s" % valid_id, + headers=valid_header, + json=valid_project_data, + follow_redirects=True) assert HTTPStatus.OK == response.status_code fake_project == json.loads(response.data) repository_update_mock.assert_called_once_with(str(valid_id), changes=valid_project_data, - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_update_project_should_reject_bad_request(client: FlaskClient, mocker: MockFixture): +def test_update_project_should_reject_bad_request(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): from time_tracker_api.projects.projects_namespace import project_dao invalid_project_data = valid_project_data.copy() invalid_project_data.update({ @@ -139,13 +173,19 @@ def test_update_project_should_reject_bad_request(client: FlaskClient, mocker: M return_value=fake_project) valid_id = fake.random_int(1, 9999) - response = client.put("/projects/%s" % valid_id, json=invalid_project_data, follow_redirects=True) + response = client.put("/projects/%s" % valid_id, + headers=valid_header, + json=invalid_project_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): +def test_update_project_should_return_not_found_with_invalid_id(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.projects.projects_namespace import project_dao from werkzeug.exceptions import NotFound @@ -156,16 +196,20 @@ def test_update_project_should_return_not_found_with_invalid_id(client: FlaskCli side_effect=NotFound) response = client.put("/projects/%s" % invalid_id, + headers=valid_header, json=valid_project_data, follow_redirects=True) assert HTTPStatus.NOT_FOUND == response.status_code repository_update_mock.assert_called_once_with(str(invalid_id), changes=valid_project_data, - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_delete_project_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture): +def test_delete_project_should_succeed_with_valid_id(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.projects.projects_namespace import project_dao valid_id = fake.random_int(1, 9999) @@ -174,15 +218,20 @@ def test_delete_project_should_succeed_with_valid_id(client: FlaskClient, mocker 'delete', return_value=None) - response = client.delete("/projects/%s" % valid_id, follow_redirects=True) + response = client.delete("/projects/%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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) -def test_delete_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture): +def test_delete_project_should_return_not_found_with_invalid_id(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.projects.projects_namespace import project_dao from werkzeug.exceptions import NotFound @@ -192,15 +241,19 @@ def test_delete_project_should_return_not_found_with_invalid_id(client: FlaskCli 'delete', side_effect=NotFound) - response = client.delete("/projects/%s" % invalid_id, follow_redirects=True) + response = client.delete("/projects/%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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) def test_delete_project_should_return_unprocessable_entity_for_invalid_id_format(client: FlaskClient, - mocker: MockFixture): + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.projects.projects_namespace import project_dao from werkzeug.exceptions import UnprocessableEntity @@ -210,8 +263,10 @@ def test_delete_project_should_return_unprocessable_entity_for_invalid_id_format 'delete', side_effect=UnprocessableEntity) - response = client.delete("/projects/%s" % invalid_id, follow_redirects=True) + response = client.delete("/projects/%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), - partition_key_value=current_user_tenant_id()) + partition_key_value=tenant_id) diff --git a/tests/time_tracker_api/time_entries/time_entries_namespace_test.py b/tests/time_tracker_api/time_entries/time_entries_namespace_test.py index 34d34259..f3fe83bb 100644 --- a/tests/time_tracker_api/time_entries/time_entries_namespace_test.py +++ b/tests/time_tracker_api/time_entries/time_entries_namespace_test.py @@ -8,7 +8,6 @@ from pytest_mock import MockFixture from commons.data_access_layer.cosmos_db import current_datetime -from time_tracker_api.security import current_user_tenant_id fake = Faker() @@ -30,7 +29,8 @@ def test_create_time_entry_with_invalid_date_range_should_raise_bad_request_error(client: FlaskClient, - mocker: MockFixture): + mocker: MockFixture, + valid_header: dict): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao repository_container_create_item_mock = mocker.patch.object(time_entries_dao.repository.container, 'create_item', @@ -40,14 +40,18 @@ def test_create_time_entry_with_invalid_date_range_should_raise_bad_request_erro invalid_time_entry_input.update({ "end_date": str(yesterday.isoformat()) }) - response = client.post("/time-entries", json=invalid_time_entry_input, follow_redirects=True) + response = client.post("/time-entries", + headers=valid_header, + json=invalid_time_entry_input, + follow_redirects=True) assert HTTPStatus.BAD_REQUEST == response.status_code repository_container_create_item_mock.assert_not_called() def test_create_time_entry_with_end_date_in_future_should_raise_bad_request_error(client: FlaskClient, - mocker: MockFixture): + mocker: MockFixture, + valid_header: dict): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao repository_container_create_item_mock = mocker.patch.object(time_entries_dao.repository.container, 'create_item', @@ -56,25 +60,35 @@ def test_create_time_entry_with_end_date_in_future_should_raise_bad_request_erro invalid_time_entry_input.update({ "end_date": str(fake.future_datetime().isoformat()) }) - response = client.post("/time-entries", json=invalid_time_entry_input, follow_redirects=True) + response = client.post("/time-entries", + headers=valid_header, + json=invalid_time_entry_input, + follow_redirects=True) assert HTTPStatus.BAD_REQUEST == response.status_code repository_container_create_item_mock.assert_not_called() -def test_create_time_entry_should_succeed_with_valid_request(client: FlaskClient, mocker: MockFixture): +def test_create_time_entry_should_succeed_with_valid_request(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao repository_create_mock = mocker.patch.object(time_entries_dao.repository, 'create', return_value=fake_time_entry) - response = client.post("/time-entries", json=valid_time_entry_input, follow_redirects=True) + response = client.post("/time-entries", + headers=valid_header, + json=valid_time_entry_input, + follow_redirects=True) assert HTTPStatus.CREATED == response.status_code repository_create_mock.assert_called_once() -def test_create_time_entry_should_reject_bad_request(client: FlaskClient, mocker: MockFixture): +def test_create_time_entry_should_reject_bad_request(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao invalid_time_entry_input = valid_time_entry_input.copy() invalid_time_entry_input.update({ @@ -84,43 +98,57 @@ def test_create_time_entry_should_reject_bad_request(client: FlaskClient, mocker 'create', return_value=fake_time_entry) - response = client.post("/time-entries", json=invalid_time_entry_input, follow_redirects=True) + response = client.post("/time-entries", + headers=valid_header, + json=invalid_time_entry_input, + follow_redirects=True) assert HTTPStatus.BAD_REQUEST == response.status_code repository_create_mock.assert_not_called() -def test_list_all_time_entries(client: FlaskClient, mocker: MockFixture): +def test_list_all_time_entries(client: FlaskClient, + mocker: MockFixture, + valid_header: dict): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao repository_find_all_mock = mocker.patch.object(time_entries_dao.repository, 'find_all', return_value=[]) - response = client.get("/time-entries", follow_redirects=True) + response = client.get("/time-entries", + headers=valid_header, + follow_redirects=True) assert HTTPStatus.OK == response.status_code assert [] == json.loads(response.data) repository_find_all_mock.assert_called_once() -def test_get_time_entry_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture): +def test_get_time_entry_should_succeed_with_valid_id(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao repository_find_mock = mocker.patch.object(time_entries_dao.repository, 'find', return_value=fake_time_entry) valid_id = fake.random_int(1, 9999) - response = client.get("/time-entries/%s" % valid_id, follow_redirects=True) + response = client.get("/time-entries/%s" % valid_id, + headers=valid_header, + follow_redirects=True) assert HTTPStatus.OK == response.status_code fake_time_entry == json.loads(response.data) repository_find_mock.assert_called_once_with(str(valid_id), - partition_key_value=current_user_tenant_id(), + partition_key_value=tenant_id, peeker=ANY) def test_get_time_entry_should_response_with_unprocessable_entity_for_invalid_id_format(client: FlaskClient, - mocker: MockFixture): + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao from werkzeug.exceptions import UnprocessableEntity @@ -130,15 +158,20 @@ def test_get_time_entry_should_response_with_unprocessable_entity_for_invalid_id 'find', side_effect=UnprocessableEntity) - response = client.get("/time-entries/%s" % invalid_id, follow_redirects=True) + response = client.get("/time-entries/%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), - partition_key_value=current_user_tenant_id(), + partition_key_value=tenant_id, peeker=ANY) -def test_update_time_entry_should_succeed_with_valid_data(client: FlaskClient, mocker: MockFixture): +def test_update_time_entry_should_succeed_with_valid_data(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao repository_update_mock = mocker.patch.object(time_entries_dao.repository, 'partial_update', @@ -146,6 +179,7 @@ def test_update_time_entry_should_succeed_with_valid_data(client: FlaskClient, m valid_id = fake.random_int(1, 9999) response = client.put("/time-entries/%s" % valid_id, + headers=valid_header, json=valid_time_entry_input, follow_redirects=True) @@ -153,11 +187,14 @@ def test_update_time_entry_should_succeed_with_valid_data(client: FlaskClient, m fake_time_entry == json.loads(response.data) repository_update_mock.assert_called_once_with(str(valid_id), changes=valid_time_entry_input, - partition_key_value=current_user_tenant_id(), + partition_key_value=tenant_id, peeker=ANY) -def test_update_time_entry_should_reject_bad_request(client: FlaskClient, mocker: MockFixture): +def test_update_time_entry_should_reject_bad_request(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao invalid_time_entry_data = valid_time_entry_input.copy() invalid_time_entry_data.update({ @@ -169,6 +206,7 @@ def test_update_time_entry_should_reject_bad_request(client: FlaskClient, mocker valid_id = fake.random_int(1, 9999) response = client.put("/time-entries/%s" % valid_id, + headers=valid_header, json=invalid_time_entry_data, follow_redirects=True) @@ -176,7 +214,10 @@ def test_update_time_entry_should_reject_bad_request(client: FlaskClient, mocker repository_update_mock.assert_not_called() -def test_update_time_entry_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture): +def test_update_time_entry_should_return_not_found_with_invalid_id(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao from werkzeug.exceptions import NotFound repository_update_mock = mocker.patch.object(time_entries_dao.repository, @@ -185,34 +226,42 @@ def test_update_time_entry_should_return_not_found_with_invalid_id(client: Flask invalid_id = fake.random_int(1, 9999) response = client.put("/time-entries/%s" % invalid_id, + headers=valid_header, json=valid_time_entry_input, follow_redirects=True) assert HTTPStatus.NOT_FOUND == response.status_code repository_update_mock.assert_called_once_with(str(invalid_id), changes=valid_time_entry_input, - partition_key_value=current_user_tenant_id(), + partition_key_value=tenant_id, peeker=ANY) -def test_delete_time_entry_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture): +def test_delete_time_entry_should_succeed_with_valid_id(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao repository_remove_mock = mocker.patch.object(time_entries_dao.repository, 'delete', return_value=None) valid_id = fake.random_int(1, 9999) - response = client.delete("/time-entries/%s" % valid_id, follow_redirects=True) + response = client.delete("/time-entries/%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), - partition_key_value=current_user_tenant_id(), + partition_key_value=tenant_id, peeker=ANY) def test_delete_time_entry_should_return_not_found_with_invalid_id(client: FlaskClient, - mocker: MockFixture): + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao from werkzeug.exceptions import NotFound repository_remove_mock = mocker.patch.object(time_entries_dao.repository, @@ -220,16 +269,20 @@ def test_delete_time_entry_should_return_not_found_with_invalid_id(client: Flask side_effect=NotFound) invalid_id = fake.random_int(1, 9999) - response = client.delete("/time-entries/%s" % invalid_id, follow_redirects=True) + response = client.delete("/time-entries/%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), - partition_key_value=current_user_tenant_id(), + partition_key_value=tenant_id, peeker=ANY) def test_delete_time_entry_should_return_unprocessable_entity_for_invalid_id_format(client: FlaskClient, - mocker: MockFixture): + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao from werkzeug.exceptions import UnprocessableEntity repository_remove_mock = mocker.patch.object(time_entries_dao.repository, @@ -237,31 +290,41 @@ def test_delete_time_entry_should_return_unprocessable_entity_for_invalid_id_for side_effect=UnprocessableEntity) invalid_id = fake.word() - response = client.delete("/time-entries/%s" % invalid_id, follow_redirects=True) + response = client.delete("/time-entries/%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), - partition_key_value=current_user_tenant_id(), + partition_key_value=tenant_id, peeker=ANY) -def test_stop_time_entry_with_valid_id(client: FlaskClient, mocker: MockFixture): +def test_stop_time_entry_with_valid_id(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao repository_update_mock = mocker.patch.object(time_entries_dao.repository, 'partial_update', return_value=fake_time_entry) valid_id = fake.random_int(1, 9999) - response = client.post("/time-entries/%s/stop" % valid_id, follow_redirects=True) + response = client.post("/time-entries/%s/stop" % valid_id, + headers=valid_header, + follow_redirects=True) assert HTTPStatus.OK == response.status_code repository_update_mock.assert_called_once_with(str(valid_id), changes={"end_date": mocker.ANY}, - partition_key_value=current_user_tenant_id(), + partition_key_value=tenant_id, peeker=ANY) -def test_stop_time_entry_with_id_with_invalid_format(client: FlaskClient, mocker: MockFixture): +def test_stop_time_entry_with_id_with_invalid_format(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao from werkzeug.exceptions import UnprocessableEntity repository_update_mock = mocker.patch.object(time_entries_dao.repository, @@ -269,32 +332,42 @@ def test_stop_time_entry_with_id_with_invalid_format(client: FlaskClient, mocker side_effect=UnprocessableEntity) invalid_id = fake.word() - response = client.post("/time-entries/%s/stop" % invalid_id, follow_redirects=True) + response = client.post("/time-entries/%s/stop" % invalid_id, + headers=valid_header, + follow_redirects=True) assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code repository_update_mock.assert_called_once_with(invalid_id, changes={"end_date": ANY}, - partition_key_value=current_user_tenant_id(), + partition_key_value=tenant_id, peeker=ANY) -def test_restart_time_entry_with_valid_id(client: FlaskClient, mocker: MockFixture): +def test_restart_time_entry_with_valid_id(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao repository_update_mock = mocker.patch.object(time_entries_dao.repository, 'partial_update', return_value=fake_time_entry) valid_id = fake.random_int(1, 9999) - response = client.post("/time-entries/%s/restart" % valid_id, follow_redirects=True) + response = client.post("/time-entries/%s/restart" % valid_id, + headers=valid_header, + follow_redirects=True) assert HTTPStatus.OK == response.status_code repository_update_mock.assert_called_once_with(str(valid_id), changes={"end_date": None}, - partition_key_value=current_user_tenant_id(), + partition_key_value=tenant_id, peeker=ANY) -def test_restart_time_entry_with_id_with_invalid_format(client: FlaskClient, mocker: MockFixture): +def test_restart_time_entry_with_id_with_invalid_format(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao from werkzeug.exceptions import UnprocessableEntity repository_update_mock = mocker.patch.object(time_entries_dao.repository, @@ -303,36 +376,47 @@ def test_restart_time_entry_with_id_with_invalid_format(client: FlaskClient, moc peeker=ANY) invalid_id = fake.word() - response = client.post("/time-entries/%s/restart" % invalid_id, follow_redirects=True) + response = client.post("/time-entries/%s/restart" % invalid_id, + headers=valid_header, + follow_redirects=True) assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code repository_update_mock.assert_called_once_with(invalid_id, changes={"end_date": None}, - partition_key_value=current_user_tenant_id(), + partition_key_value=tenant_id, peeker=ANY) -def test_get_running_should_call_find_running(client: FlaskClient, mocker: MockFixture): +def test_get_running_should_call_find_running(client: FlaskClient, + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao repository_update_mock = mocker.patch.object(time_entries_dao.repository, 'find_running', return_value=fake_time_entry) - response = client.get("/time-entries/running", follow_redirects=True) + response = client.get("/time-entries/running", + headers=valid_header, + follow_redirects=True) assert HTTPStatus.OK == response.status_code assert json.loads(response.data) is not None - repository_update_mock.assert_called_once_with(partition_key_value=current_user_tenant_id()) + repository_update_mock.assert_called_once_with(partition_key_value=tenant_id) def test_get_running_should_return_not_found_if_find_running_throws_StopIteration(client: FlaskClient, - mocker: MockFixture): + mocker: MockFixture, + valid_header: dict, + tenant_id: str): from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao repository_update_mock = mocker.patch.object(time_entries_dao.repository, 'find_running', side_effect=StopIteration) - response = client.get("/time-entries/running", follow_redirects=True) + response = client.get("/time-entries/running", + headers=valid_header, + follow_redirects=True) assert HTTPStatus.NOT_FOUND == response.status_code - repository_update_mock.assert_called_once_with(partition_key_value=current_user_tenant_id()) + repository_update_mock.assert_called_once_with(partition_key_value=tenant_id) From be9fec2b59dc0a5c125c1156b057b61038b578f2 Mon Sep 17 00:00:00 2001 From: EliuX Date: Wed, 22 Apr 2020 13:18:50 -0500 Subject: [PATCH 06/13] fix: Ignore deleted-running time entry --- tests/conftest.py | 2 +- time_tracker_api/time_entries/time_entries_model.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5fad738c..92781c03 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -154,7 +154,7 @@ def running_time_entry(time_entry_repository: TimeEntryCosmosDBRepository, yield created_time_entry - time_entry_repository.delete(id=created_time_entry.id, + time_entry_repository.delete_permanently(id=created_time_entry.id, partition_key_value=tenant_id) diff --git a/time_tracker_api/time_entries/time_entries_model.py b/time_tracker_api/time_entries/time_entries_model.py index f2646edc..3c2bd098 100644 --- a/time_tracker_api/time_entries/time_entries_model.py +++ b/time_tracker_api/time_entries/time_entries_model.py @@ -111,9 +111,11 @@ def find_running(self, partition_key_value: str, mapper: Callable = None): result = self.container.query_items( query=""" SELECT * from c - WHERE NOT IS_DEFINED(c.end_date) OR c.end_date = null + WHERE (NOT IS_DEFINED(c.end_date) OR c.end_date = null) {visibility_condition} OFFSET 0 LIMIT 1 - """, + """.format( + visibility_condition=self.create_sql_condition_for_visibility(True), + ), partition_key=partition_key_value, max_item_count=1) From fe27bbb22d5f0e04333938c58eaff0909a083e6e Mon Sep 17 00:00:00 2001 From: semantic-release Date: Wed, 22 Apr 2020 21:03:52 +0000 Subject: [PATCH 07/13] 0.5.0 Automatically generated by python-semantic-release --- time_tracker_api/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index f0ede3d3..2b8877c5 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.4.1' +__version__ = '0.5.0' From 9a951ae631036961a0c291649bf15975d3b33493 Mon Sep 17 00:00:00 2001 From: roberto Date: Wed, 22 Apr 2020 13:26:10 -0500 Subject: [PATCH 08/13] chore: add pre-commit library and config to enforce semantic commit messages --- .pre-commit-config.yaml | 8 +++++ .../git_hooks/enforce_semantic_commit_msg.py | 29 +++++++++++++++++++ requirements/time_tracker_api/dev.txt | 3 ++ 3 files changed, 40 insertions(+) create mode 100644 .pre-commit-config.yaml create mode 100644 commons/git_hooks/enforce_semantic_commit_msg.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..797fb4b0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,8 @@ +repos: +- repo: local + hooks: + - id: semantic-commit-msg + name: Check semantic commit message format + entry: python ./commons/git_hooks/enforce_semantic_commit_msg.py + language: python + stages : [commit-msg] diff --git a/commons/git_hooks/enforce_semantic_commit_msg.py b/commons/git_hooks/enforce_semantic_commit_msg.py new file mode 100644 index 00000000..c01a3605 --- /dev/null +++ b/commons/git_hooks/enforce_semantic_commit_msg.py @@ -0,0 +1,29 @@ +import re +import sys + +ERROR_MSG = """ +Commit failed! +Please use semantic commit message format. Examples: + 'feat: Applying some changes' + 'fix: Fixing something broken' + 'feat(config): Fix something in config files' + +For more details in commit message format, review https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits +""" + +SUCCESS_MSG = "Commit succeed!. Semantic commit message is correct." + +COMMIT_MSG_REGEX = r'(build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert)(\([\w\-]+\))?:\s.*' + + +# Get the commit message file +commit_msg_file = open(sys.argv[1]) # The first argument is the file +commit_msg = commit_msg_file.read() + + +if re.match(COMMIT_MSG_REGEX, commit_msg) is None: + print(ERROR_MSG) + sys.exit(1) + +print(SUCCESS_MSG) +sys.exit(0) diff --git a/requirements/time_tracker_api/dev.txt b/requirements/time_tracker_api/dev.txt index c242e4a1..3a26760c 100644 --- a/requirements/time_tracker_api/dev.txt +++ b/requirements/time_tracker_api/dev.txt @@ -13,3 +13,6 @@ pytest-mock==2.0.0 # Coverage coverage==4.5.1 + +# Git hooks +pre-commit==2.2.0 \ No newline at end of file From 7d3388330527a7ff363ac1e06d08f2cc917e7846 Mon Sep 17 00:00:00 2001 From: roberto Date: Thu, 23 Apr 2020 09:53:55 -0500 Subject: [PATCH 09/13] docs: add instructions to enable enforce-semantic-commit-message git hook --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a4fdaff7..c49b99f3 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,9 @@ automatically [pip](https://pip.pypa.io/en/stable/) as well. Remember to do it with Python 3. - + +- Run `pre-commit install`. For more details, check out Development > Git hooks. + ### How to use it - Set the env var `FLASK_APP` to `time_tracker_api` and start the app: @@ -74,6 +76,16 @@ DateTime strings in Azure Cosmos DB is `YYYY-MM-DDThh:mm:ss.fffffffZ` which foll ## Development +### Git hooks +We use [pre-commit](https://github.com/pre-commit/pre-commit) library to manage local git hooks, as developers we just need to run in our virtual environment: + +``` +pre-commit install +``` +With this command the library will take configuration from `.pre-commit-config.yaml` and will set up the hooks by us. + +Currently, we only have a hook to enforce semantic commit message. + ### Test We are using [Pytest](https://docs.pytest.org/en/latest/index.html) for tests. The tests are located in the package `tests` and use the [conventions for python test discovery](https://docs.pytest.org/en/latest/goodpractices.html#test-discovery). From 53613dfcdf692515667b6fe2ac0e4bfa5f7420c6 Mon Sep 17 00:00:00 2001 From: EliuX Date: Wed, 22 Apr 2020 17:53:04 -0500 Subject: [PATCH 10/13] fix: Close #96 Autogenerate start_date if not present --- commons/data_access_layer/cosmos_db.py | 17 +++++++++--- tests/conftest.py | 11 +++++--- .../time_entries/time_entries_model_test.py | 2 +- .../time_entries/time_entries_model.py | 26 ++++++++++++------- .../time_entries/time_entries_namespace.py | 8 +++--- 5 files changed, 42 insertions(+), 22 deletions(-) diff --git a/commons/data_access_layer/cosmos_db.py b/commons/data_access_layer/cosmos_db.py index 4778d5d2..f8f6c391 100644 --- a/commons/data_access_layer/cosmos_db.py +++ b/commons/data_access_layer/cosmos_db.py @@ -139,7 +139,8 @@ def create(self, data: dict, mapper: Callable = None): function_mapper = self.get_mapper_or_dict(mapper) return function_mapper(self.container.create_item(body=data)) - def find(self, id: str, partition_key_value, peeker: 'function' = None, visible_only=True, mapper: Callable = None): + def find(self, id: str, partition_key_value, peeker: 'function' = None, + visible_only=True, mapper: Callable = None): found_item = self.container.read_item(id, partition_key_value) if peeker: peeker(found_item) @@ -201,7 +202,7 @@ def get_page_size_or(self, custom_page_size: int) -> int: def on_create(self, new_item_data: dict): if new_item_data.get('id') is None: - new_item_data['id'] = str(uuid.uuid4()) + new_item_data['id'] = generate_uuid4() def on_update(self, update_item_data: dict): pass @@ -245,17 +246,25 @@ def __init__(self, status_code: int, description: str = None): self.description = description -def current_datetime(): +def current_datetime() -> datetime: return datetime.utcnow() -def datetime_str(value: datetime): +def datetime_str(value: datetime) -> str: if value is not None: return value.isoformat() else: return None +def current_datetime_str() -> str: + return datetime_str(current_datetime()) + + +def generate_uuid4() -> str: + return str(uuid.uuid4()) + + def init_app(app: Flask) -> None: global cosmos_helper cosmos_helper = CosmosDBFacade.from_flask_config(app) diff --git a/tests/conftest.py b/tests/conftest.py index 97b80b65..f42686eb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -129,7 +129,13 @@ def another_item(cosmos_db_repository: CosmosDBRepository, tenant_id: str) -> di @pytest.fixture(scope="module") -def time_entry_repository() -> TimeEntryCosmosDBRepository: +def time_entry_repository(app: Flask) -> TimeEntryCosmosDBRepository: + with app.app_context(): + from commons.data_access_layer.cosmos_db import init_app, cosmos_helper + + if cosmos_helper is None: + init_app(app) + return TimeEntryCosmosDBRepository() @@ -139,12 +145,11 @@ def running_time_entry(time_entry_repository: TimeEntryCosmosDBRepository, tenant_id: str): created_time_entry = time_entry_repository.create({ "project_id": fake.uuid4(), - "start_date": datetime_str(current_datetime()), "owner_id": owner_id, "tenant_id": tenant_id }) yield created_time_entry - time_entry_repository.delete(id=created_time_entry.id, + time_entry_repository.delete_permanently(id=created_time_entry.id, partition_key_value=tenant_id) diff --git a/tests/time_tracker_api/time_entries/time_entries_model_test.py b/tests/time_tracker_api/time_entries/time_entries_model_test.py index 2b58c036..43bd998c 100644 --- a/tests/time_tracker_api/time_entries/time_entries_model_test.py +++ b/tests/time_tracker_api/time_entries/time_entries_model_test.py @@ -48,7 +48,7 @@ def test_find_interception_with_date_range_should_find(start_date: datetime, partition_key_value=tenant_id) assert result is not None - assert len(result) >= 0 + assert len(result) > 0 assert any([existing_item.id == item.id for item in result]) finally: time_entry_repository.delete_permanently(existing_item.id, partition_key_value=existing_item.tenant_id) diff --git a/time_tracker_api/time_entries/time_entries_model.py b/time_tracker_api/time_entries/time_entries_model.py index f2646edc..1b63202b 100644 --- a/time_tracker_api/time_entries/time_entries_model.py +++ b/time_tracker_api/time_entries/time_entries_model.py @@ -5,8 +5,8 @@ from azure.cosmos import PartitionKey from flask_restplus._http import HTTPStatus -from commons.data_access_layer.cosmos_db import CosmosDBDao, CosmosDBRepository, CustomError, CosmosDBModel, \ - current_datetime, datetime_str +from commons.data_access_layer.cosmos_db import CosmosDBDao, CosmosDBRepository, CustomError, current_datetime_str, \ + CosmosDBModel from commons.data_access_layer.database import CRUDDao from time_tracker_api.security import current_user_id @@ -34,15 +34,15 @@ def find_running(self): @dataclass() class TimeEntryCosmosDBModel(CosmosDBModel): - id: str - description: str project_id: str - activity_id: str + start_date: str owner_id: str + id: str tenant_id: str + description: str = field(default=None) + activity_id: str = field(default=None) uri: str = field(default=None) technologies: List[str] = field(default_factory=list) - start_date: str = field(default_factory=current_datetime) end_date: str = field(default=None) deleted: str = field(default=None) @@ -76,6 +76,10 @@ def create_sql_ignore_id_condition(id: str): def on_create(self, new_item_data: dict): CosmosDBRepository.on_create(self, new_item_data) + + if new_item_data.get("start_date") is None: + new_item_data['start_date'] = current_datetime_str() + self.validate_data(new_item_data) def on_update(self, updated_item_data: dict): @@ -88,7 +92,7 @@ def find_interception_with_date_range(self, start_date, end_date, owner_id, part params = self.append_conditions_values([ {"name": "@partition_key_value", "value": partition_key_value}, {"name": "@start_date", "value": start_date}, - {"name": "@end_date", "value": end_date or datetime_str(current_datetime())}, + {"name": "@end_date", "value": end_date or current_datetime_str()}, {"name": "@ignore_id", "value": ignore_id}, ], conditions) result = self.container.query_items( @@ -121,15 +125,17 @@ def find_running(self, partition_key_value: str, mapper: Callable = None): return function_mapper(next(result)) def validate_data(self, data): + start_date = data.get('start_date') + if data.get('end_date') is not None: - if data['end_date'] <= data.get('start_date'): + if data['end_date'] <= start_date: raise CustomError(HTTPStatus.BAD_REQUEST, description="You must end the time entry after it started") - if data['end_date'] >= datetime_str(current_datetime()): + if data['end_date'] >= current_datetime_str(): raise CustomError(HTTPStatus.BAD_REQUEST, description="You cannot end a time entry in the future") - collision = self.find_interception_with_date_range(start_date=data.get('start_date'), + collision = self.find_interception_with_date_range(start_date=start_date, end_date=data.get('end_date'), owner_id=data.get('owner_id'), partition_key_value=data.get('tenant_id'), diff --git a/time_tracker_api/time_entries/time_entries_namespace.py b/time_tracker_api/time_entries/time_entries_namespace.py index a7bc2a42..e107729b 100644 --- a/time_tracker_api/time_entries/time_entries_namespace.py +++ b/time_tracker_api/time_entries/time_entries_namespace.py @@ -4,7 +4,7 @@ from flask_restplus import fields, Resource, Namespace from flask_restplus._http import HTTPStatus -from commons.data_access_layer.cosmos_db import current_datetime, datetime_str +from commons.data_access_layer.cosmos_db import current_datetime, datetime_str, current_datetime_str from commons.data_access_layer.database import COMMENTS_MAX_LENGTH from time_tracker_api.api import common_fields, UUID_REGEX from time_tracker_api.time_entries.time_entries_model import create_dao @@ -25,7 +25,7 @@ 'start_date': fields.DateTime( dt_format='iso8601', title='Start date', - required=True, + required=False, description='When the user started doing this activity', example=datetime_str(current_datetime() - timedelta(days=1)), ), @@ -48,7 +48,7 @@ title='End date', required=False, description='When the user ended doing this activity', - example=datetime_str(current_datetime()), + example=current_datetime_str(), ), 'uri': fields.String( title='Uniform Resource identifier', @@ -165,7 +165,7 @@ class StopTimeEntry(Resource): def post(self, id): """Stop a running time entry""" return time_entries_dao.update(id, { - 'end_date': datetime_str(current_datetime()) + 'end_date': current_datetime_str() }) From 5012ff7d31073d2f90a9303409d04305198cbb4d Mon Sep 17 00:00:00 2001 From: semantic-release Date: Thu, 23 Apr 2020 15:33:14 +0000 Subject: [PATCH 11/13] 0.5.1 Automatically generated by python-semantic-release --- time_tracker_api/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index 2b8877c5..93b60a1d 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.5.0' +__version__ = '0.5.1' From a1b1a6ab121050f7efb904807b022fd8ca73a6df Mon Sep 17 00:00:00 2001 From: Rene Enriquez Date: Tue, 21 Apr 2020 13:03:13 -0500 Subject: [PATCH 12/13] fix: #91 adding filter by attributes --- commons/data_access_layer/cosmos_db.py | 4 ++-- .../activities/activities_namespace_test.py | 2 +- .../customers/customers_namespace_test.py | 15 +++++++++++++++ .../project_types/project_types_namespace_test.py | 2 +- .../projects/projects_namespace_test.py | 2 +- .../activities/activities_namespace.py | 3 ++- time_tracker_api/customers/customers_namespace.py | 3 ++- .../project_types/project_types_namespace.py | 3 ++- time_tracker_api/projects/projects_namespace.py | 3 ++- 9 files changed, 28 insertions(+), 9 deletions(-) diff --git a/commons/data_access_layer/cosmos_db.py b/commons/data_access_layer/cosmos_db.py index f8f6c391..8c17abf0 100644 --- a/commons/data_access_layer/cosmos_db.py +++ b/commons/data_access_layer/cosmos_db.py @@ -217,8 +217,8 @@ class CosmosDBDao(CRUDDao): def __init__(self, repository: CosmosDBRepository): self.repository = repository - def get_all(self) -> list: - return self.repository.find_all(partition_key_value=self.partition_key_value) + def get_all(self, conditions: []) -> list: + return self.repository.find_all(partition_key_value=self.partition_key_value, conditions= conditions) def get(self, id): return self.repository.find(id, partition_key_value=self.partition_key_value) diff --git a/tests/time_tracker_api/activities/activities_namespace_test.py b/tests/time_tracker_api/activities/activities_namespace_test.py index 508e7f88..e5cbad43 100644 --- a/tests/time_tracker_api/activities/activities_namespace_test.py +++ b/tests/time_tracker_api/activities/activities_namespace_test.py @@ -258,4 +258,4 @@ def test_delete_activity_should_return_422_for_invalid_id_format(client: FlaskCl assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code repository_remove_mock.assert_called_once_with(str(invalid_id), - partition_key_value=tenant_id) + partition_key_value=tenant_id) \ 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 1777cbe8..f36fc3a9 100644 --- a/tests/time_tracker_api/customers/customers_namespace_test.py +++ b/tests/time_tracker_api/customers/customers_namespace_test.py @@ -3,6 +3,7 @@ from flask.testing import FlaskClient from flask_restplus._http import HTTPStatus from pytest_mock import MockFixture +from werkzeug.datastructures import ImmutableMultiDict fake = Faker() @@ -68,6 +69,20 @@ def test_list_all_customers(client: FlaskClient, assert [] == json_data repository_find_all_mock.assert_called_once() +def test_list_all_customers_with_conditions(client: FlaskClient, mocker: MockFixture): + 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?a=b", 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(conditions=ImmutableMultiDict({'a': 'b'}), + partition_key_value='ioet') + def test_get_customer_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture, diff --git a/tests/time_tracker_api/project_types/project_types_namespace_test.py b/tests/time_tracker_api/project_types/project_types_namespace_test.py index 3d0b3135..0e7385fa 100644 --- a/tests/time_tracker_api/project_types/project_types_namespace_test.py +++ b/tests/time_tracker_api/project_types/project_types_namespace_test.py @@ -269,4 +269,4 @@ def test_delete_project_should_return_unprocessable_entity_for_invalid_id_format assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code repository_remove_mock.assert_called_once_with(str(invalid_id), - partition_key_value=tenant_id) + partition_key_value=tenant_id) \ No newline at end of file diff --git a/tests/time_tracker_api/projects/projects_namespace_test.py b/tests/time_tracker_api/projects/projects_namespace_test.py index 6c7ff5eb..0f042e2f 100644 --- a/tests/time_tracker_api/projects/projects_namespace_test.py +++ b/tests/time_tracker_api/projects/projects_namespace_test.py @@ -269,4 +269,4 @@ def test_delete_project_should_return_unprocessable_entity_for_invalid_id_format assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code repository_remove_mock.assert_called_once_with(str(invalid_id), - partition_key_value=tenant_id) + partition_key_value=tenant_id) \ No newline at end of file diff --git a/time_tracker_api/activities/activities_namespace.py b/time_tracker_api/activities/activities_namespace.py index d4f42f20..5d79cb5a 100644 --- a/time_tracker_api/activities/activities_namespace.py +++ b/time_tracker_api/activities/activities_namespace.py @@ -1,6 +1,7 @@ from faker import Faker from flask_restplus import fields, Resource, Namespace from flask_restplus._http import HTTPStatus +from flask import request from time_tracker_api.activities.activities_model import create_dao from time_tracker_api.api import common_fields @@ -44,7 +45,7 @@ class Activities(Resource): @ns.marshal_list_with(activity) def get(self): """List all activities""" - return activity_dao.get_all() + return activity_dao.get_all(conditions=request.args) @ns.doc('create_activity') @ns.response(HTTPStatus.CONFLICT, 'This activity already exists') diff --git a/time_tracker_api/customers/customers_namespace.py b/time_tracker_api/customers/customers_namespace.py index f3b01fcc..e4a8dc57 100644 --- a/time_tracker_api/customers/customers_namespace.py +++ b/time_tracker_api/customers/customers_namespace.py @@ -1,6 +1,7 @@ from faker import Faker from flask_restplus import Namespace, Resource, fields from flask_restplus._http import HTTPStatus +from flask import request from time_tracker_api.api import common_fields from time_tracker_api.customers.customers_model import create_dao @@ -45,7 +46,7 @@ class Customers(Resource): @ns.marshal_list_with(customer) def get(self): """List all customers""" - return customer_dao.get_all() + return customer_dao.get_all(conditions=request.args) @ns.doc('create_customer') @ns.response(HTTPStatus.CONFLICT, 'This customer already exists') diff --git a/time_tracker_api/project_types/project_types_namespace.py b/time_tracker_api/project_types/project_types_namespace.py index 0e6c0ac6..c01ffb9d 100644 --- a/time_tracker_api/project_types/project_types_namespace.py +++ b/time_tracker_api/project_types/project_types_namespace.py @@ -1,6 +1,7 @@ from faker import Faker from flask_restplus import Namespace, Resource, fields from flask_restplus._http import HTTPStatus +from flask import request from time_tracker_api.api import common_fields, UUID_REGEX from time_tracker_api.project_types.project_types_model import create_dao @@ -60,7 +61,7 @@ class ProjectTypes(Resource): @ns.marshal_list_with(project_type) def get(self): """List all project types""" - return project_type_dao.get_all() + return project_type_dao.get_all(conditions=request.args) @ns.doc('create_project_type') @ns.response(HTTPStatus.CONFLICT, 'This project type already exists') diff --git a/time_tracker_api/projects/projects_namespace.py b/time_tracker_api/projects/projects_namespace.py index 5fe4ad1e..e0fdfc6d 100644 --- a/time_tracker_api/projects/projects_namespace.py +++ b/time_tracker_api/projects/projects_namespace.py @@ -1,6 +1,7 @@ from faker import Faker from flask_restplus import Namespace, Resource, fields from flask_restplus._http import HTTPStatus +from flask import request from time_tracker_api.api import common_fields, UUID_REGEX from time_tracker_api.projects.projects_model import create_dao @@ -61,7 +62,7 @@ class Projects(Resource): @ns.marshal_list_with(project) def get(self): """List all projects""" - return project_dao.get_all() + return project_dao.get_all(conditions=request.args) @ns.doc('create_project') @ns.response(HTTPStatus.CONFLICT, 'This project already exists') From beb8c84f36be17eac9e88979211cb01ec53573df Mon Sep 17 00:00:00 2001 From: Rene Enriquez Date: Thu, 23 Apr 2020 14:55:33 -0500 Subject: [PATCH 13/13] fix: #91 applying filters criteria on query parameter --- time_tracker_api/api.py | 10 +++++++++- time_tracker_api/collections/dictionary_utils.py | 16 ++++++++++++++++ time_tracker_api/projects/projects_namespace.py | 10 +++++++--- 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 time_tracker_api/collections/dictionary_utils.py diff --git a/time_tracker_api/api.py b/time_tracker_api/api.py index b269136b..df554c13 100644 --- a/time_tracker_api/api.py +++ b/time_tracker_api/api.py @@ -1,7 +1,7 @@ from azure.cosmos.exceptions import CosmosResourceExistsError, CosmosResourceNotFoundError, CosmosHttpResponseError from faker import Faker from flask import current_app as app -from flask_restplus import Api, fields +from flask_restplus import Api, fields, reqparse from flask_restplus._http import HTTPStatus from commons.data_access_layer.cosmos_db import CustomError @@ -18,6 +18,14 @@ security="TimeTracker JWT", ) +# Filters +def create_attributes_filter(attributes_filter): + filter_attributes_parser = reqparse.RequestParser() + for attribute in attributes_filter: + filter_attributes_parser.add_argument(f'filters[{attribute}]', location='args') + + return filter_attributes_parser + # For matching UUIDs UUID_REGEX = '[0-9a-f]{8}\-[0-9a-f]{4}\-4[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}' diff --git a/time_tracker_api/collections/dictionary_utils.py b/time_tracker_api/collections/dictionary_utils.py new file mode 100644 index 00000000..d6682860 --- /dev/null +++ b/time_tracker_api/collections/dictionary_utils.py @@ -0,0 +1,16 @@ +import re + +def remove_none_values(dictionary): + dictionary_with_values = {} + for key, value in dictionary.items(): + if value is not None: + dictionary_with_values.update({key: value}) + return dictionary_with_values + + +def remove_filters_wrapper_from_keys(dictionary): + dictionary_with_unwrapped_keys = {} + for key, value in dictionary.items(): + unwrapped_key = re.search('\\[(.+?)\\]', key).groups()[0] + dictionary_with_unwrapped_keys.update({unwrapped_key: value}) + return dictionary_with_unwrapped_keys diff --git a/time_tracker_api/projects/projects_namespace.py b/time_tracker_api/projects/projects_namespace.py index e0fdfc6d..6767c2a6 100644 --- a/time_tracker_api/projects/projects_namespace.py +++ b/time_tracker_api/projects/projects_namespace.py @@ -1,9 +1,10 @@ from faker import Faker from flask_restplus import Namespace, Resource, fields from flask_restplus._http import HTTPStatus -from flask import request -from time_tracker_api.api import common_fields, UUID_REGEX +from time_tracker_api.api import common_fields, UUID_REGEX, create_attributes_filter + +from time_tracker_api.collections.dictionary_utils import remove_none_values, remove_filters_wrapper_from_keys from time_tracker_api.projects.projects_model import create_dao faker = Faker() @@ -55,14 +56,17 @@ project_dao = create_dao() +attributes_filter = create_attributes_filter(['customer_id']) @ns.route('') class Projects(Resource): @ns.doc('list_projects') @ns.marshal_list_with(project) + @ns.expect(attributes_filter) def get(self): """List all projects""" - return project_dao.get_all(conditions=request.args) + conditions = remove_none_values(attributes_filter.parse_args()) + return project_dao.get_all(conditions=remove_filters_wrapper_from_keys(conditions)) @ns.doc('create_project') @ns.response(HTTPStatus.CONFLICT, 'This project already exists')