From 8ffa516e91240d18433ce8a7ca05c2af39939784 Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Mon, 5 Jan 2026 13:11:49 -0800 Subject: [PATCH 1/9] Add in endpoint to create_org that includes contact --- auth-api/src/auth_api/resources/endpoints.py | 2 + .../src/auth_api/resources/v2/__init__.py | 37 ++++++++ auth-api/src/auth_api/resources/v2/org.py | 89 +++++++++++++++++++ .../src/auth_api/schemas/schemas/contact.json | 30 ++++--- .../src/auth_api/utils/endpoints_enums.py | 1 + auth-api/tests/unit/api/test_org.py | 18 ++++ 6 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 auth-api/src/auth_api/resources/v2/__init__.py create mode 100644 auth-api/src/auth_api/resources/v2/org.py diff --git a/auth-api/src/auth_api/resources/endpoints.py b/auth-api/src/auth_api/resources/endpoints.py index 208bf0dbe..acaf1d674 100644 --- a/auth-api/src/auth_api/resources/endpoints.py +++ b/auth-api/src/auth_api/resources/endpoints.py @@ -18,6 +18,7 @@ from flask import Blueprint, Flask # noqa: I001 from .v1 import v1_endpoint +from .v2 import v2_endpoint TEST_BLUEPRINT = Blueprint("TEST", __name__, url_prefix="/test") @@ -39,6 +40,7 @@ def init_app(self, app: Flask): def _mount_endpoints(self): """Mount the endpoints of the system.""" v1_endpoint.init_app(self.app) + v2_endpoint.init_app(self.app) if os.getenv("FLASK_ENV", "production") in ["development", "testing"]: self.app.register_blueprint(TEST_BLUEPRINT) diff --git a/auth-api/src/auth_api/resources/v2/__init__.py b/auth-api/src/auth_api/resources/v2/__init__.py new file mode 100644 index 000000000..64556e41e --- /dev/null +++ b/auth-api/src/auth_api/resources/v2/__init__.py @@ -0,0 +1,37 @@ +# Copyright © 2025 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Exposes all of the resource endpoints mounted in Flask-Blueprints - V2.""" + +from flask import Flask # noqa: TC002 + +from .org import bp as org_bp + + +class V2Endpoint: + """Setup all the V2 Endpoints.""" + + def __init__(self): + """Create the endpoint setup, without initializations.""" + self.app: Flask | None = None + + def init_app(self, app): + """Register and initialize the Endpoint setup.""" + if not app: + raise Exception("Cannot initialize without a Flask App.") # pylint: disable=broad-exception-raised + + self.app = app + self.app.register_blueprint(org_bp) + + +v2_endpoint = V2Endpoint() diff --git a/auth-api/src/auth_api/resources/v2/org.py b/auth-api/src/auth_api/resources/v2/org.py new file mode 100644 index 000000000..f073fdda1 --- /dev/null +++ b/auth-api/src/auth_api/resources/v2/org.py @@ -0,0 +1,89 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""API endpoints for managing an Org resource - V2.""" + +from http import HTTPStatus + +from flask import Blueprint, request +from flask_cors import cross_origin + +from auth_api.exceptions import BusinessException +from auth_api.schemas import utils as schema_utils +from auth_api.services import Org as OrgService +from auth_api.services import User as UserService +from auth_api.utils.auth import jwt as _jwt +from auth_api.utils.endpoints_enums import EndpointEnum +from auth_api.utils.role_validator import validate_roles +from auth_api.utils.roles import Role + +bp = Blueprint("ORGS_V2", __name__, url_prefix=f"{EndpointEnum.API_V2.value}/orgs") + + +@bp.route("", methods=["POST"]) +@cross_origin(origins="*") +@validate_roles( + allowed_roles=[Role.PUBLIC_USER.value, Role.STAFF_CREATE_ACCOUNTS.value, Role.SYSTEM.value], + not_allowed_roles=[Role.ANONYMOUS_USER.value], +) +@_jwt.has_one_of_roles([Role.PUBLIC_USER.value, Role.STAFF_CREATE_ACCOUNTS.value, Role.SYSTEM.value]) +def post_organization(): + """Post a new org with contact using the request body. + + Creates an organization and then adds a contact if provided. + Validates both org and contact schemas before processing. + """ + request_json = request.get_json() + if not request_json: + return {"message": "Request body cannot be empty"}, HTTPStatus.BAD_REQUEST + + org_info = request_json.copy() + contact_info = org_info.pop("contact", None) + + if contact_info: + org_info.pop("mailingAddress", None) + + valid_format, errors = schema_utils.validate(org_info, "org") + if not valid_format: + return {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST + + if contact_info: + valid_contact_format, contact_errors = schema_utils.validate(contact_info, "contact") + if not valid_contact_format: + return {"message": schema_utils.serialize(contact_errors)}, HTTPStatus.BAD_REQUEST + + try: + user = UserService.find_by_jwt_token() + if user is None: + response, status = {"message": "Not authorized to perform this action"}, HTTPStatus.UNAUTHORIZED + return response, status + + org = OrgService.create_org(org_info, user.identifier) + org_dict = org.as_dict() + org_id = org_dict.get("id") + + if contact_info and org_id: + contact = OrgService.add_contact(org_id, contact_info) + org_dict["contact"] = contact.as_dict() + + response, status = org_dict, HTTPStatus.CREATED + except BusinessException as exception: + response, status = ( + { + "code": exception.code, + "message": exception.message, + "detail": exception.detail, + }, + exception.status_code, + ) + return response, status diff --git a/auth-api/src/auth_api/schemas/schemas/contact.json b/auth-api/src/auth_api/schemas/schemas/contact.json index f1c9aec9c..b1c52dc14 100644 --- a/auth-api/src/auth_api/schemas/schemas/contact.json +++ b/auth-api/src/auth_api/schemas/schemas/contact.json @@ -14,7 +14,8 @@ "examples": [ "123 Roundabout Lane" ], - "pattern": "^(.*)$" + "pattern": "^(.*)$", + "maxLength": 250 }, "streetAdditional": { "$id": "#/properties/street_additional", @@ -24,7 +25,8 @@ "examples": [ "Unit 1" ], - "pattern": "^(.*)$" + "pattern": "^(.*)$", + "maxLength": 250 }, "city": { "$id": "#/properties/city", @@ -34,7 +36,8 @@ "examples": [ "Victoria" ], - "pattern": "^(.*)$" + "pattern": "^(.*)$", + "maxLength": 100 }, "region": { "$id": "#/properties/region", @@ -44,7 +47,8 @@ "examples": [ "British Columbia" ], - "pattern": "^(.*)$" + "pattern": "^(.*)$", + "maxLength": 100 }, "country": { "$id": "#/properties/country", @@ -54,7 +58,8 @@ "examples": [ "CA" ], - "pattern": "^(.*)$" + "pattern": "^(.*)$", + "maxLength": 20 }, "postalCode": { "$id": "#/properties/postal_code", @@ -64,7 +69,8 @@ "examples": [ "V1A 1A1" ], - "pattern": "^(.*)$" + "pattern": "^(.*)$", + "maxLength": 15 }, "deliveryInstructions": { "$id": "#/properties/delivery_instructions", @@ -74,7 +80,8 @@ "examples": [ "Ring buzzer 123" ], - "pattern": "^(.*)$" + "pattern": "^(.*)$", + "maxLength": 4096 }, "phone": { "$id": "#/properties/phone", @@ -84,7 +91,8 @@ "examples": [ "111-222-3333" ], - "pattern": "^(.*)$" + "pattern": "^(.*)$", + "maxLength": 15 }, "phoneExtension": { "$id": "#/properties/phone_extension", @@ -94,7 +102,8 @@ "examples": [ "123" ], - "pattern": "^(.*)$" + "pattern": "^(.*)$", + "maxLength": 10 }, "email": { "$id": "#/properties/email", @@ -104,7 +113,8 @@ "examples": [ "abc123@mail.com" ], - "pattern": "(^[a-zA-Z0-9!#$%&'*+-/=?^_`{|.]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$)" + "pattern": "(^[a-zA-Z0-9!#$%&'*+-/=?^_`{|.]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$)", + "maxLength": 100 } } } diff --git a/auth-api/src/auth_api/utils/endpoints_enums.py b/auth-api/src/auth_api/utils/endpoints_enums.py index cd284d1d5..d690431f6 100644 --- a/auth-api/src/auth_api/utils/endpoints_enums.py +++ b/auth-api/src/auth_api/utils/endpoints_enums.py @@ -20,6 +20,7 @@ class EndpointEnum(str, Enum): """Endpoint route url paths.""" API_V1 = "/api/v1" + API_V2 = "/api/v2" API = "/api" TEST_API = "/test" DEFAULT_API = API_V1 diff --git a/auth-api/tests/unit/api/test_org.py b/auth-api/tests/unit/api/test_org.py index bf7f924ac..0f646eac3 100644 --- a/auth-api/tests/unit/api/test_org.py +++ b/auth-api/tests/unit/api/test_org.py @@ -101,6 +101,24 @@ def test_add_org(client, jwt, session, keycloak_mock, org_info): # pylint:disab assert schema_utils.validate(rv.json, "org_response")[0] +def test_add_org_v2_with_contact(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument + """Assert that an org can be POSTed with contact using v2 endpoint.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.public_user_role) + rv = client.post("/api/v1/users", headers=headers, content_type="application/json") + + org_data = TestOrgInfo.org1.copy() + org_data["contact"] = TestContactInfo.contact1 + + rv = client.post("/api/v2/orgs", data=json.dumps(org_data), headers=headers, content_type="application/json") + assert rv.status_code == HTTPStatus.CREATED + dictionary = json.loads(rv.data) + assert dictionary["name"] == TestOrgInfo.org1["name"] + assert "contact" in dictionary + assert dictionary["contact"]["email"] == TestContactInfo.contact1["email"] + assert dictionary["contact"]["phone"] == TestContactInfo.contact1["phone"] + assert schema_utils.validate(rv.json, "org_response")[0] + + @pytest.mark.parametrize( "org_info", [ From 84e5c834d0ea0884d662bc59a80e5262880346e9 Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Mon, 5 Jan 2026 13:22:37 -0800 Subject: [PATCH 2/9] DRY --- auth-api/src/auth_api/resources/org_utils.py | 105 +++++++++++++++++++ auth-api/src/auth_api/resources/v1/org.py | 49 ++++----- auth-api/src/auth_api/resources/v2/org.py | 52 ++++----- auth-api/tests/unit/api/test_org.py | 24 +++++ 4 files changed, 171 insertions(+), 59 deletions(-) create mode 100644 auth-api/src/auth_api/resources/org_utils.py diff --git a/auth-api/src/auth_api/resources/org_utils.py b/auth-api/src/auth_api/resources/org_utils.py new file mode 100644 index 000000000..0f9c5e8ab --- /dev/null +++ b/auth-api/src/auth_api/resources/org_utils.py @@ -0,0 +1,105 @@ +# Copyright © 2019 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Shared utility functions for org endpoints.""" + +from http import HTTPStatus + +from auth_api.exceptions import BusinessException +from auth_api.schemas import utils as schema_utils +from auth_api.services import Org as OrgService +from auth_api.services import User as UserService + + +def validate_and_get_user(): + """Validate request and get authenticated user. + + Returns: + tuple: (user, error_response, error_status) or (user, None, None) if successful + """ + user = UserService.find_by_jwt_token() + if user is None: + return None, {"message": "Not authorized to perform this action"}, HTTPStatus.UNAUTHORIZED + return user, None, None + + +def validate_org_schema(org_info): + """Validate organization schema. + + Args: + org_info: Organization data to validate + + Returns: + tuple: (is_valid, errors) - (True, None) if valid, (False, error_dict) if invalid + """ + valid_format, errors = schema_utils.validate(org_info, "org") + if not valid_format: + return False, {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST + return True, None, None + + +def validate_contact_schema(contact_info): + """Validate contact schema. + + Args: + contact_info: Contact data to validate + + Returns: + tuple: (is_valid, errors) - (True, None) if valid, (False, error_dict) if invalid + """ + valid_format, errors = schema_utils.validate(contact_info, "contact") + if not valid_format: + return False, {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST + return True, None, None + + +def create_org_with_validation(org_info, user_id): + """Create an organization with validation. + + Args: + org_info: Organization data + user_id: User identifier + + Returns: + tuple: (org_dict, None, None) on success or (None, error_response, error_status) on failure + """ + try: + org = OrgService.create_org(org_info, user_id) + return org.as_dict(), None, None + except BusinessException as exception: + return ( + None, + { + "code": exception.code, + "message": exception.message, + "detail": exception.detail, + }, + exception.status_code, + ) + + +def add_contact_with_validation(org_id, contact_info): + """Add contact to organization with validation. + + Args: + org_id: Organization ID + contact_info: Contact data + + Returns: + tuple: (contact_dict, None, None) on success or (None, error_response, error_status) on failure + """ + try: + contact = OrgService.add_contact(org_id, contact_info) + return contact.as_dict(), None, None + except BusinessException as exception: + return None, {"code": exception.code, "message": exception.message}, exception.status_code diff --git a/auth-api/src/auth_api/resources/v1/org.py b/auth-api/src/auth_api/resources/v1/org.py index fb254c539..3e62cc88e 100644 --- a/auth-api/src/auth_api/resources/v1/org.py +++ b/auth-api/src/auth_api/resources/v1/org.py @@ -27,6 +27,7 @@ from auth_api.models.dataclass import Affiliation as AffiliationData from auth_api.models.dataclass import AffiliationSearchDetails, DeleteAffiliationRequest, SimpleOrgSearch from auth_api.models.org import OrgSearch # noqa: I001 +from auth_api.resources import org_utils from auth_api.schemas import InvitationSchema, MembershipSchema from auth_api.schemas import utils as schema_utils from auth_api.services import Affidavit as AffidavitService @@ -158,25 +159,19 @@ def post_organization(): If the org already exists, update the attributes. """ request_json = request.get_json() - valid_format, errors = schema_utils.validate(request_json, "org") - if not valid_format: - return {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST - try: - user = UserService.find_by_jwt_token() - if user is None: - response, status = {"message": "Not authorized to perform this action"}, HTTPStatus.UNAUTHORIZED - return response, status - response, status = OrgService.create_org(request_json, user.identifier).as_dict(), HTTPStatus.CREATED - except BusinessException as exception: - response, status = ( - { - "code": exception.code, - "message": exception.message, - "detail": exception.detail, - }, - exception.status_code, - ) - return response, status + is_valid, error_response, error_status = org_utils.validate_org_schema(request_json) + if not is_valid: + return error_response, error_status + + user, error_response, error_status = org_utils.validate_and_get_user() + if error_response: + return error_response, error_status + + org_dict, error_response, error_status = org_utils.create_org_with_validation(request_json, user.identifier) + if error_response: + return error_response, error_status + + return org_dict, HTTPStatus.CREATED @bp.route("/", methods=["GET", "OPTIONS"]) @@ -326,15 +321,15 @@ def get(org_id): def post_organization_contact(org_id): """Create a new contact for the specified org.""" request_json = request.get_json() - valid_format, errors = schema_utils.validate(request_json, "contact") - if not valid_format: - return {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST + is_valid, error_response, error_status = org_utils.validate_contact_schema(request_json) + if not is_valid: + return error_response, error_status - try: - response, status = OrgService.add_contact(org_id, request_json).as_dict(), HTTPStatus.CREATED - except BusinessException as exception: - response, status = {"code": exception.code, "message": exception.message}, exception.status_code - return response, status + contact_dict, error_response, error_status = org_utils.add_contact_with_validation(org_id, request_json) + if error_response: + return error_response, error_status + + return contact_dict, HTTPStatus.CREATED @bp.route("//contacts", methods=["PUT"]) diff --git a/auth-api/src/auth_api/resources/v2/org.py b/auth-api/src/auth_api/resources/v2/org.py index f073fdda1..7620bbe26 100644 --- a/auth-api/src/auth_api/resources/v2/org.py +++ b/auth-api/src/auth_api/resources/v2/org.py @@ -18,10 +18,7 @@ from flask import Blueprint, request from flask_cors import cross_origin -from auth_api.exceptions import BusinessException -from auth_api.schemas import utils as schema_utils -from auth_api.services import Org as OrgService -from auth_api.services import User as UserService +from auth_api.resources import org_utils from auth_api.utils.auth import jwt as _jwt from auth_api.utils.endpoints_enums import EndpointEnum from auth_api.utils.role_validator import validate_roles @@ -53,37 +50,28 @@ def post_organization(): if contact_info: org_info.pop("mailingAddress", None) - valid_format, errors = schema_utils.validate(org_info, "org") - if not valid_format: - return {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST + is_valid, error_response, error_status = org_utils.validate_org_schema(org_info) + if not is_valid: + return error_response, error_status if contact_info: - valid_contact_format, contact_errors = schema_utils.validate(contact_info, "contact") - if not valid_contact_format: - return {"message": schema_utils.serialize(contact_errors)}, HTTPStatus.BAD_REQUEST + is_valid, error_response, error_status = org_utils.validate_contact_schema(contact_info) + if not is_valid: + return error_response, error_status - try: - user = UserService.find_by_jwt_token() - if user is None: - response, status = {"message": "Not authorized to perform this action"}, HTTPStatus.UNAUTHORIZED - return response, status + user, error_response, error_status = org_utils.validate_and_get_user() + if error_response: + return error_response, error_status - org = OrgService.create_org(org_info, user.identifier) - org_dict = org.as_dict() - org_id = org_dict.get("id") + org_dict, error_response, error_status = org_utils.create_org_with_validation(org_info, user.identifier) + if error_response: + return error_response, error_status - if contact_info and org_id: - contact = OrgService.add_contact(org_id, contact_info) - org_dict["contact"] = contact.as_dict() + org_id = org_dict.get("id") + if contact_info and org_id: + contact_dict, error_response, error_status = org_utils.add_contact_with_validation(org_id, contact_info) + if error_response: + return error_response, error_status + org_dict["contact"] = contact_dict - response, status = org_dict, HTTPStatus.CREATED - except BusinessException as exception: - response, status = ( - { - "code": exception.code, - "message": exception.message, - "detail": exception.detail, - }, - exception.status_code, - ) - return response, status + return org_dict, HTTPStatus.CREATED diff --git a/auth-api/tests/unit/api/test_org.py b/auth-api/tests/unit/api/test_org.py index 0f646eac3..a5cf6caa7 100644 --- a/auth-api/tests/unit/api/test_org.py +++ b/auth-api/tests/unit/api/test_org.py @@ -119,6 +119,30 @@ def test_add_org_v2_with_contact(client, jwt, session, keycloak_mock): # pylint assert schema_utils.validate(rv.json, "org_response")[0] +def test_add_org_v2_with_contact_fields_too_long(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument + """Assert that an org with contact fields exceeding maxLength returns 400.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.public_user_role) + rv = client.post("/api/v1/users", headers=headers, content_type="application/json") + + org_data = TestOrgInfo.org1.copy() + org_data["contact"] = { + "email": "a" * 101, + "phone": "1" * 16, + "phoneExtension": "1" * 11, + "street": "a" * 251, + "streetAdditional": "a" * 251, + "city": "a" * 101, + "region": "a" * 101, + "country": "a" * 21, + "postalCode": "a" * 16, + "deliveryInstructions": "a" * 4097, + } + + rv = client.post("/api/v2/orgs", data=json.dumps(org_data), headers=headers, content_type="application/json") + assert rv.status_code == HTTPStatus.BAD_REQUEST + assert "message" in rv.json + + @pytest.mark.parametrize( "org_info", [ From f4e32454599d940d2abd81a1b37af06ff29bed8c Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Mon, 5 Jan 2026 13:29:12 -0800 Subject: [PATCH 3/9] fix comments --- auth-api/src/auth_api/resources/org_utils.py | 44 +++----------------- 1 file changed, 5 insertions(+), 39 deletions(-) diff --git a/auth-api/src/auth_api/resources/org_utils.py b/auth-api/src/auth_api/resources/org_utils.py index 0f9c5e8ab..21fe6b23e 100644 --- a/auth-api/src/auth_api/resources/org_utils.py +++ b/auth-api/src/auth_api/resources/org_utils.py @@ -22,11 +22,7 @@ def validate_and_get_user(): - """Validate request and get authenticated user. - - Returns: - tuple: (user, error_response, error_status) or (user, None, None) if successful - """ + """Validate request and get authenticated user.""" user = UserService.find_by_jwt_token() if user is None: return None, {"message": "Not authorized to perform this action"}, HTTPStatus.UNAUTHORIZED @@ -34,14 +30,7 @@ def validate_and_get_user(): def validate_org_schema(org_info): - """Validate organization schema. - - Args: - org_info: Organization data to validate - - Returns: - tuple: (is_valid, errors) - (True, None) if valid, (False, error_dict) if invalid - """ + """Validate organization schema.""" valid_format, errors = schema_utils.validate(org_info, "org") if not valid_format: return False, {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST @@ -49,14 +38,7 @@ def validate_org_schema(org_info): def validate_contact_schema(contact_info): - """Validate contact schema. - - Args: - contact_info: Contact data to validate - - Returns: - tuple: (is_valid, errors) - (True, None) if valid, (False, error_dict) if invalid - """ + """Validate contact schema.""" valid_format, errors = schema_utils.validate(contact_info, "contact") if not valid_format: return False, {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST @@ -64,15 +46,7 @@ def validate_contact_schema(contact_info): def create_org_with_validation(org_info, user_id): - """Create an organization with validation. - - Args: - org_info: Organization data - user_id: User identifier - - Returns: - tuple: (org_dict, None, None) on success or (None, error_response, error_status) on failure - """ + """Create an organization with validation.""" try: org = OrgService.create_org(org_info, user_id) return org.as_dict(), None, None @@ -89,15 +63,7 @@ def create_org_with_validation(org_info, user_id): def add_contact_with_validation(org_id, contact_info): - """Add contact to organization with validation. - - Args: - org_id: Organization ID - contact_info: Contact data - - Returns: - tuple: (contact_dict, None, None) on success or (None, error_response, error_status) on failure - """ + """Add contact to organization with validation.""" try: contact = OrgService.add_contact(org_id, contact_info) return contact.as_dict(), None, None From 3ce39e33e5c6f4fc79dd31d8267b95b604220e8c Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Mon, 5 Jan 2026 13:37:21 -0800 Subject: [PATCH 4/9] Fix old tables, expand to 20 --- auth-api/migrations/versions/2024_09_20_aa74003de9d8_.py | 2 +- auth-api/migrations/versions/414773385d34_initialization_mvp.py | 2 +- auth-api/migrations/versions/5cf63f567a62_adding_version.py | 2 +- auth-api/src/auth_api/models/contact.py | 2 +- auth-api/src/auth_api/schemas/schemas/contact.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/auth-api/migrations/versions/2024_09_20_aa74003de9d8_.py b/auth-api/migrations/versions/2024_09_20_aa74003de9d8_.py index 01d592db9..055640280 100644 --- a/auth-api/migrations/versions/2024_09_20_aa74003de9d8_.py +++ b/auth-api/migrations/versions/2024_09_20_aa74003de9d8_.py @@ -118,7 +118,7 @@ def upgrade(): sa.Column("country", sa.String(length=20), autoincrement=False, nullable=True), sa.Column("postal_code", sa.String(length=15), autoincrement=False, nullable=True), sa.Column("delivery_instructions", sa.String(length=4096), autoincrement=False, nullable=True), - sa.Column("phone", sa.String(length=15), autoincrement=False, nullable=True), + sa.Column("phone", sa.String(length=20), autoincrement=False, nullable=True), sa.Column("phone_extension", sa.String(length=10), autoincrement=False, nullable=True), sa.Column("email", sa.String(length=100), autoincrement=False, nullable=True), sa.Column("entity_id", sa.Integer(), autoincrement=False, nullable=True), diff --git a/auth-api/migrations/versions/414773385d34_initialization_mvp.py b/auth-api/migrations/versions/414773385d34_initialization_mvp.py index b67484543..54df4733d 100644 --- a/auth-api/migrations/versions/414773385d34_initialization_mvp.py +++ b/auth-api/migrations/versions/414773385d34_initialization_mvp.py @@ -73,7 +73,7 @@ def upgrade(): sa.Column('country', sa.String(length=2), nullable=True), sa.Column('postal_code', sa.String(length=10), nullable=True), sa.Column('delivery_instructions', sa.String(length=4096), nullable=True), - sa.Column('phone', sa.String(length=15), nullable=True), + sa.Column('phone', sa.String(length=20), nullable=True), sa.Column('phone_extension', sa.String(length=10), nullable=True), sa.Column('email', sa.String(length=100), nullable=True), sa.Column('entity_id', sa.Integer(), nullable=True), diff --git a/auth-api/migrations/versions/5cf63f567a62_adding_version.py b/auth-api/migrations/versions/5cf63f567a62_adding_version.py index c612e6f9b..80caf75ba 100644 --- a/auth-api/migrations/versions/5cf63f567a62_adding_version.py +++ b/auth-api/migrations/versions/5cf63f567a62_adding_version.py @@ -133,7 +133,7 @@ def upgrade(): sa.Column('country', sa.String(length=20), autoincrement=False, nullable=True), sa.Column('postal_code', sa.String(length=10), autoincrement=False, nullable=True), sa.Column('delivery_instructions', sa.String(length=4096), autoincrement=False, nullable=True), - sa.Column('phone', sa.String(length=15), autoincrement=False, nullable=True), + sa.Column('phone', sa.String(length=20), autoincrement=False, nullable=True), sa.Column('phone_extension', sa.String(length=10), autoincrement=False, nullable=True), sa.Column('email', sa.String(length=100), autoincrement=False, nullable=True), sa.Column('entity_id', sa.Integer(), autoincrement=False, nullable=True), diff --git a/auth-api/src/auth_api/models/contact.py b/auth-api/src/auth_api/models/contact.py index 4fad87fdd..970e995ec 100644 --- a/auth-api/src/auth_api/models/contact.py +++ b/auth-api/src/auth_api/models/contact.py @@ -37,7 +37,7 @@ class Contact(Versioned, BaseModel): # pylint: disable=too-few-public-methods country = Column("country", String(20)) postal_code = Column("postal_code", String(15)) delivery_instructions = Column("delivery_instructions", String(4096)) - phone = Column("phone", String(15)) + phone = Column("phone", String(20)) phone_extension = Column("phone_extension", String(10)) email = Column("email", String(100)) # MVP contact has been migrated over to the contact linking table (revised data model) diff --git a/auth-api/src/auth_api/schemas/schemas/contact.json b/auth-api/src/auth_api/schemas/schemas/contact.json index b1c52dc14..5a8676db2 100644 --- a/auth-api/src/auth_api/schemas/schemas/contact.json +++ b/auth-api/src/auth_api/schemas/schemas/contact.json @@ -92,7 +92,7 @@ "111-222-3333" ], "pattern": "^(.*)$", - "maxLength": 15 + "maxLength": 20 }, "phoneExtension": { "$id": "#/properties/phone_extension", From 252b99fc5cfcb4bfee8bd1d73b7f2c245244aa60 Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Mon, 12 Jan 2026 13:13:17 -0800 Subject: [PATCH 5/9] Add in another unit test for no contact --- auth-api/src/auth_api/services/keycloak.py | 8 +- auth-api/tests/unit/api/test_org.py | 20 ++-- auth-api/tests/unit/services/test_keycloak.py | 91 ++++++------------- 3 files changed, 45 insertions(+), 74 deletions(-) diff --git a/auth-api/src/auth_api/services/keycloak.py b/auth-api/src/auth_api/services/keycloak.py index da0c32323..f44dd2126 100644 --- a/auth-api/src/auth_api/services/keycloak.py +++ b/auth-api/src/auth_api/services/keycloak.py @@ -46,6 +46,7 @@ class KCRequestConfig: realm: str timeout: int + class KeycloakService: """For Keycloak services.""" @@ -313,7 +314,6 @@ def kc_user_to_dict(user: dict) -> dict: """Return a dict representation of the KC user.""" return {"firstName": user["firstName"], "lastName": user["lastName"], "email": user["email"]} - @staticmethod def get_default_kc_request_config(): """Return default request configuration for Keycloak API calls.""" @@ -322,7 +322,7 @@ def get_default_kc_request_config(): return KCRequestConfig( base_url=config.get("KEYCLOAK_BASE_URL"), realm=config.get("KEYCLOAK_REALMNAME"), - timeout=config.get("CONNECT_TIMEOUT", 60) + timeout=config.get("CONNECT_TIMEOUT", 60), ) @staticmethod @@ -380,8 +380,8 @@ def get_user_emails_with_role(role: str): for kc_group in kc_groups: group_members.extend( [ - KeycloakService.kc_user_to_dict(member) for member in - KeycloakService.get_keycloak_members_for_group(kc_group["id"]) + KeycloakService.kc_user_to_dict(member) + for member in KeycloakService.get_keycloak_members_for_group(kc_group["id"]) ] ) users.extend(group_members) diff --git a/auth-api/tests/unit/api/test_org.py b/auth-api/tests/unit/api/test_org.py index a5cf6caa7..b629aa57b 100644 --- a/auth-api/tests/unit/api/test_org.py +++ b/auth-api/tests/unit/api/test_org.py @@ -101,21 +101,29 @@ def test_add_org(client, jwt, session, keycloak_mock, org_info): # pylint:disab assert schema_utils.validate(rv.json, "org_response")[0] -def test_add_org_v2_with_contact(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument +@pytest.mark.parametrize( + "has_contact, expected_status_code", [("with_contact", HTTPStatus.CREATED), ("no_contact", HTTPStatus.CREATED)] +) +def test_add_org_v2_with_contact(client, jwt, session, keycloak_mock, has_contact, expected_status_code): # pylint:disable=unused-argument """Assert that an org can be POSTed with contact using v2 endpoint.""" headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.public_user_role) rv = client.post("/api/v1/users", headers=headers, content_type="application/json") org_data = TestOrgInfo.org1.copy() - org_data["contact"] = TestContactInfo.contact1 + + if has_contact == "with_contact": + org_data["contact"] = TestContactInfo.contact1 rv = client.post("/api/v2/orgs", data=json.dumps(org_data), headers=headers, content_type="application/json") - assert rv.status_code == HTTPStatus.CREATED + assert rv.status_code == expected_status_code dictionary = json.loads(rv.data) assert dictionary["name"] == TestOrgInfo.org1["name"] - assert "contact" in dictionary - assert dictionary["contact"]["email"] == TestContactInfo.contact1["email"] - assert dictionary["contact"]["phone"] == TestContactInfo.contact1["phone"] + if has_contact == "with_contact": + assert "contact" in dictionary + assert dictionary["contact"]["email"] == TestContactInfo.contact1["email"] + assert dictionary["contact"]["phone"] == TestContactInfo.contact1["phone"] + else: + assert "contact" not in dictionary # Ensure no contact is present in the response assert schema_utils.validate(rv.json, "org_response")[0] diff --git a/auth-api/tests/unit/services/test_keycloak.py b/auth-api/tests/unit/services/test_keycloak.py index e7d136c13..e7606a5f0 100644 --- a/auth-api/tests/unit/services/test_keycloak.py +++ b/auth-api/tests/unit/services/test_keycloak.py @@ -16,6 +16,7 @@ Test-Suite to ensure that the Business Service is working as expected. """ + from unittest.mock import patch import pytest @@ -287,60 +288,30 @@ def test_service_account_by_client_name(session): @patch("auth_api.services.keycloak.KeycloakService.get_keycloak_groups_for_role") @patch("auth_api.services.keycloak.KeycloakService.get_keycloak_members_for_group") def test_get_user_emails_with_role( - mock_get_group_members, - mock_get_groups_for_role, - mock_get_users_for_role, - mock_get_admin_token, - session, + mock_get_group_members, + mock_get_groups_for_role, + mock_get_users_for_role, + mock_get_admin_token, + session, ): """Test get_user_emails_with_role returns users from both direct role assignment and group membership.""" mock_get_admin_token.return_value = "mock_token" # Set up for direct role assignment to a user and test handling logic when users are returned kc_users = [ - { - "id": "user1", - "firstName": "John", - "lastName": "Doe", - "email": "john.doe@example.com" - }, - { - "id": "user2", - "firstName": "Jane", - "lastName": "Smith", - "email": "jane.smith@example.com" - } + {"id": "user1", "firstName": "John", "lastName": "Doe", "email": "john.doe@example.com"}, + {"id": "user2", "firstName": "Jane", "lastName": "Smith", "email": "jane.smith@example.com"}, ] # Groups and group members to test logic when inherited group roles exist - kc_groups = [ - {"id": "group1", "name": "test_group_1"}, - {"id": "group2", "name": "test_group_2"} - ] + kc_groups = [{"id": "group1", "name": "test_group_1"}, {"id": "group2", "name": "test_group_2"}] group1_members = [ - { - "id": "user3", - "firstName": "Bob", - "lastName": "Johnson", - "email": "bob.johnson@example.com" - }, - { - "id": "user1", - "firstName": "John", - "lastName": "Doe", - "email": "john.doe@example.com" - } + {"id": "user3", "firstName": "Bob", "lastName": "Johnson", "email": "bob.johnson@example.com"}, + {"id": "user1", "firstName": "John", "lastName": "Doe", "email": "john.doe@example.com"}, ] - group2_members = [ - { - "id": "user4", - "firstName": "Alice", - "lastName": "Brown", - "email": "alice.brown@example.com" - } - ] + group2_members = [{"id": "user4", "firstName": "Alice", "lastName": "Brown", "email": "alice.brown@example.com"}] mock_get_users_for_role.return_value = kc_users mock_get_groups_for_role.return_value = kc_groups @@ -367,7 +338,7 @@ def mock_get_members(group_id): "john.doe@example.com", "jane.smith@example.com", "bob.johnson@example.com", - "alice.brown@example.com" + "alice.brown@example.com", ] for email in expected_emails: @@ -385,11 +356,11 @@ def mock_get_members(group_id): @patch("auth_api.services.keycloak.KeycloakService.get_keycloak_groups_for_role") @patch("auth_api.services.keycloak.KeycloakService.get_keycloak_members_for_group") def test_get_user_emails_with_role_empty_results( - mock_get_group_members, - mock_get_groups_for_role, - mock_get_users_for_role, - mock_get_admin_token, - session, + mock_get_group_members, + mock_get_groups_for_role, + mock_get_users_for_role, + mock_get_admin_token, + session, ): """Test get_user_emails_with_role handles empty results correctly.""" mock_get_admin_token.return_value = "mock_token" @@ -409,11 +380,11 @@ def test_get_user_emails_with_role_empty_results( @patch("auth_api.services.keycloak.KeycloakService.get_keycloak_groups_for_role") @patch("auth_api.services.keycloak.KeycloakService.get_keycloak_members_for_group") def test_get_user_emails_with_role_none_users( - mock_get_group_members, - mock_get_groups_for_role, - mock_get_users_for_role, - mock_get_admin_token, - session, + mock_get_group_members, + mock_get_groups_for_role, + mock_get_users_for_role, + mock_get_admin_token, + session, ): """Test get_user_emails_with_role handles None return from get_keycloak_users_for_role.""" mock_get_admin_token.return_value = "mock_token" @@ -422,14 +393,7 @@ def test_get_user_emails_with_role_none_users( role_groups = [{"id": "group1", "name": "test_group"}] mock_get_groups_for_role.return_value = role_groups - group_members = [ - { - "id": "user1", - "firstName": "Test", - "lastName": "User", - "email": "test.user@example.com" - } - ] + group_members = [{"id": "user1", "firstName": "Test", "lastName": "User", "email": "test.user@example.com"}] mock_get_group_members.return_value = group_members result = KEYCLOAK_SERVICE.get_user_emails_with_role("test_role") @@ -439,13 +403,12 @@ def test_get_user_emails_with_role_none_users( assert result[0]["lastName"] == "User" - @patch("auth_api.services.keycloak.KeycloakService._get_admin_token") @patch("auth_api.services.keycloak.KeycloakService.get_keycloak_users_for_role") def test_get_user_emails_with_role_nonexistent_role( - mock_get_users_for_role, - mock_get_admin_token, - session, + mock_get_users_for_role, + mock_get_admin_token, + session, ): """Test get_user_emails_with_role raises BusinessException when role doesn't exist in Keycloak.""" mock_get_admin_token.return_value = "mock_token" From 6d8e96f69bf4ecfedae53469a43e1632e5496c8b Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Wed, 28 Jan 2026 11:42:10 -0800 Subject: [PATCH 6/9] code clean up --- auth-api/src/auth_api/resources/org_utils.py | 86 +++++++++++--------- auth-api/src/auth_api/resources/v1/org.py | 30 +++---- auth-api/src/auth_api/resources/v2/org.py | 32 ++++---- 3 files changed, 75 insertions(+), 73 deletions(-) diff --git a/auth-api/src/auth_api/resources/org_utils.py b/auth-api/src/auth_api/resources/org_utils.py index 21fe6b23e..6f080624b 100644 --- a/auth-api/src/auth_api/resources/org_utils.py +++ b/auth-api/src/auth_api/resources/org_utils.py @@ -13,7 +13,9 @@ # limitations under the License. """Shared utility functions for org endpoints.""" +from dataclasses import dataclass from http import HTTPStatus +from typing import Self from auth_api.exceptions import BusinessException from auth_api.schemas import utils as schema_utils @@ -21,51 +23,59 @@ from auth_api.services import User as UserService -def validate_and_get_user(): - """Validate request and get authenticated user.""" - user = UserService.find_by_jwt_token() - if user is None: - return None, {"message": "Not authorized to perform this action"}, HTTPStatus.UNAUTHORIZED - return user, None, None +@dataclass +class Result[T]: + """Encapsulates success value or error response.""" + + value: T | None = None + error: dict | None = None + status: HTTPStatus = HTTPStatus.OK + + @property + def is_success(self) -> bool: + """Return True if the result is successful.""" + return self.error is None + + @classmethod + def success(cls, value: T) -> Self: + """Create a successful result with the given value.""" + return cls(value=value) + @classmethod + def failure(cls, message: str, status: HTTPStatus, **extras) -> Self: + """Create a failure result with the given error message and status.""" + return cls(error={"message": message, **extras}, status=status) -def validate_org_schema(org_info): - """Validate organization schema.""" - valid_format, errors = schema_utils.validate(org_info, "org") - if not valid_format: - return False, {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST - return True, None, None + +def validate_and_get_user() -> Result: + """Validate request and get authenticated user.""" + user = UserService.find_by_jwt_token() + if not user: + return Result.failure("Not authorized to perform this action", HTTPStatus.UNAUTHORIZED) + return Result.success(user) -def validate_contact_schema(contact_info): - """Validate contact schema.""" - valid_format, errors = schema_utils.validate(contact_info, "contact") - if not valid_format: - return False, {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST - return True, None, None +def validate_schema(data: dict, schema_name: str) -> Result: + """Validate data against a schema.""" + valid, errors = schema_utils.validate(data, schema_name) + if not valid: + return Result.failure(schema_utils.serialize(errors), HTTPStatus.BAD_REQUEST) + return Result.success(True) -def create_org_with_validation(org_info, user_id): - """Create an organization with validation.""" +def create_org(org_info: dict, user_id: int) -> Result: + """Create an organization.""" try: org = OrgService.create_org(org_info, user_id) - return org.as_dict(), None, None - except BusinessException as exception: - return ( - None, - { - "code": exception.code, - "message": exception.message, - "detail": exception.detail, - }, - exception.status_code, - ) - - -def add_contact_with_validation(org_id, contact_info): - """Add contact to organization with validation.""" + return Result.success(org.as_dict()) + except BusinessException as e: + return Result.failure(e.message, e.status_code, code=e.code, detail=e.detail) + + +def add_contact(org_id: int, contact_info: dict) -> Result: + """Add contact to organization.""" try: contact = OrgService.add_contact(org_id, contact_info) - return contact.as_dict(), None, None - except BusinessException as exception: - return None, {"code": exception.code, "message": exception.message}, exception.status_code + return Result.success(contact.as_dict()) + except BusinessException as e: + return Result.failure(e.message, e.status_code, code=e.code) diff --git a/auth-api/src/auth_api/resources/v1/org.py b/auth-api/src/auth_api/resources/v1/org.py index 3e62cc88e..b783bb752 100644 --- a/auth-api/src/auth_api/resources/v1/org.py +++ b/auth-api/src/auth_api/resources/v1/org.py @@ -159,19 +159,17 @@ def post_organization(): If the org already exists, update the attributes. """ request_json = request.get_json() - is_valid, error_response, error_status = org_utils.validate_org_schema(request_json) - if not is_valid: - return error_response, error_status + if not (result := org_utils.validate_schema(request_json, "org")).is_success: + return result.error, result.status - user, error_response, error_status = org_utils.validate_and_get_user() - if error_response: - return error_response, error_status + if not (result := org_utils.validate_and_get_user()).is_success: + return result.error, result.status + user = result.value - org_dict, error_response, error_status = org_utils.create_org_with_validation(request_json, user.identifier) - if error_response: - return error_response, error_status + if not (result := org_utils.create_org(request_json, user.identifier)).is_success: + return result.error, result.status - return org_dict, HTTPStatus.CREATED + return result.value, HTTPStatus.CREATED @bp.route("/", methods=["GET", "OPTIONS"]) @@ -321,15 +319,13 @@ def get(org_id): def post_organization_contact(org_id): """Create a new contact for the specified org.""" request_json = request.get_json() - is_valid, error_response, error_status = org_utils.validate_contact_schema(request_json) - if not is_valid: - return error_response, error_status + if not (result := org_utils.validate_schema(request_json, "contact")).is_success: + return result.error, result.status - contact_dict, error_response, error_status = org_utils.add_contact_with_validation(org_id, request_json) - if error_response: - return error_response, error_status + if not (result := org_utils.add_contact(org_id, request_json)).is_success: + return result.error, result.status - return contact_dict, HTTPStatus.CREATED + return result.value, HTTPStatus.CREATED @bp.route("//contacts", methods=["PUT"]) diff --git a/auth-api/src/auth_api/resources/v2/org.py b/auth-api/src/auth_api/resources/v2/org.py index 7620bbe26..b1e2c1ba7 100644 --- a/auth-api/src/auth_api/resources/v2/org.py +++ b/auth-api/src/auth_api/resources/v2/org.py @@ -50,28 +50,24 @@ def post_organization(): if contact_info: org_info.pop("mailingAddress", None) - is_valid, error_response, error_status = org_utils.validate_org_schema(org_info) - if not is_valid: - return error_response, error_status + if not (result := org_utils.validate_schema(org_info, "org")).is_success: + return result.error, result.status if contact_info: - is_valid, error_response, error_status = org_utils.validate_contact_schema(contact_info) - if not is_valid: - return error_response, error_status + if not (result := org_utils.validate_schema(contact_info, "contact")).is_success: + return result.error, result.status - user, error_response, error_status = org_utils.validate_and_get_user() - if error_response: - return error_response, error_status + if not (result := org_utils.validate_and_get_user()).is_success: + return result.error, result.status + user = result.value - org_dict, error_response, error_status = org_utils.create_org_with_validation(org_info, user.identifier) - if error_response: - return error_response, error_status + if not (result := org_utils.create_org(org_info, user.identifier)).is_success: + return result.error, result.status + org_dict = result.value - org_id = org_dict.get("id") - if contact_info and org_id: - contact_dict, error_response, error_status = org_utils.add_contact_with_validation(org_id, contact_info) - if error_response: - return error_response, error_status - org_dict["contact"] = contact_dict + if contact_info and (org_id := org_dict.get("id")): + if not (result := org_utils.add_contact(org_id, contact_info)).is_success: + return result.error, result.status + org_dict["contact"] = result.value return org_dict, HTTPStatus.CREATED From c9daa3916ff77fc23ebf922843431939dfcfbb82 Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Wed, 28 Jan 2026 11:45:01 -0800 Subject: [PATCH 7/9] fix this up --- auth-api/src/auth_api/resources/org_utils.py | 5 +++++ auth-api/src/auth_api/resources/v1/org.py | 10 +++++----- auth-api/src/auth_api/resources/v2/org.py | 10 +++++----- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/auth-api/src/auth_api/resources/org_utils.py b/auth-api/src/auth_api/resources/org_utils.py index 6f080624b..9e5fd8273 100644 --- a/auth-api/src/auth_api/resources/org_utils.py +++ b/auth-api/src/auth_api/resources/org_utils.py @@ -36,6 +36,11 @@ def is_success(self) -> bool: """Return True if the result is successful.""" return self.error is None + @property + def is_failure(self) -> bool: + """Return True if the result is a failure.""" + return self.error is not None + @classmethod def success(cls, value: T) -> Self: """Create a successful result with the given value.""" diff --git a/auth-api/src/auth_api/resources/v1/org.py b/auth-api/src/auth_api/resources/v1/org.py index b783bb752..e7d541a45 100644 --- a/auth-api/src/auth_api/resources/v1/org.py +++ b/auth-api/src/auth_api/resources/v1/org.py @@ -159,14 +159,14 @@ def post_organization(): If the org already exists, update the attributes. """ request_json = request.get_json() - if not (result := org_utils.validate_schema(request_json, "org")).is_success: + if (result := org_utils.validate_schema(request_json, "org")).is_failure: return result.error, result.status - if not (result := org_utils.validate_and_get_user()).is_success: + if (result := org_utils.validate_and_get_user()).is_failure: return result.error, result.status user = result.value - if not (result := org_utils.create_org(request_json, user.identifier)).is_success: + if (result := org_utils.create_org(request_json, user.identifier)).is_failure: return result.error, result.status return result.value, HTTPStatus.CREATED @@ -319,10 +319,10 @@ def get(org_id): def post_organization_contact(org_id): """Create a new contact for the specified org.""" request_json = request.get_json() - if not (result := org_utils.validate_schema(request_json, "contact")).is_success: + if (result := org_utils.validate_schema(request_json, "contact")).is_failure: return result.error, result.status - if not (result := org_utils.add_contact(org_id, request_json)).is_success: + if (result := org_utils.add_contact(org_id, request_json)).is_failure: return result.error, result.status return result.value, HTTPStatus.CREATED diff --git a/auth-api/src/auth_api/resources/v2/org.py b/auth-api/src/auth_api/resources/v2/org.py index b1e2c1ba7..f2a3986e3 100644 --- a/auth-api/src/auth_api/resources/v2/org.py +++ b/auth-api/src/auth_api/resources/v2/org.py @@ -50,23 +50,23 @@ def post_organization(): if contact_info: org_info.pop("mailingAddress", None) - if not (result := org_utils.validate_schema(org_info, "org")).is_success: + if (result := org_utils.validate_schema(org_info, "org")).is_failure: return result.error, result.status if contact_info: - if not (result := org_utils.validate_schema(contact_info, "contact")).is_success: + if (result := org_utils.validate_schema(contact_info, "contact")).is_failure: return result.error, result.status - if not (result := org_utils.validate_and_get_user()).is_success: + if (result := org_utils.validate_and_get_user()).is_failure: return result.error, result.status user = result.value - if not (result := org_utils.create_org(org_info, user.identifier)).is_success: + if (result := org_utils.create_org(org_info, user.identifier)).is_failure: return result.error, result.status org_dict = result.value if contact_info and (org_id := org_dict.get("id")): - if not (result := org_utils.add_contact(org_id, contact_info)).is_success: + if (result := org_utils.add_contact(org_id, contact_info)).is_failure: return result.error, result.status org_dict["contact"] = result.value From 82aac44334595b52e1e37fe73a00868816a69e14 Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Wed, 28 Jan 2026 11:45:20 -0800 Subject: [PATCH 8/9] clean up --- auth-api/src/auth_api/resources/org_utils.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/auth-api/src/auth_api/resources/org_utils.py b/auth-api/src/auth_api/resources/org_utils.py index 9e5fd8273..cb5e4a66b 100644 --- a/auth-api/src/auth_api/resources/org_utils.py +++ b/auth-api/src/auth_api/resources/org_utils.py @@ -31,11 +31,6 @@ class Result[T]: error: dict | None = None status: HTTPStatus = HTTPStatus.OK - @property - def is_success(self) -> bool: - """Return True if the result is successful.""" - return self.error is None - @property def is_failure(self) -> bool: """Return True if the result is a failure.""" From 34e921574a7ddeeab7adce1cb2df37a5a414b4b7 Mon Sep 17 00:00:00 2001 From: Travis Semple Date: Wed, 28 Jan 2026 11:45:53 -0800 Subject: [PATCH 9/9] leave in is_success --- auth-api/src/auth_api/resources/org_utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/auth-api/src/auth_api/resources/org_utils.py b/auth-api/src/auth_api/resources/org_utils.py index cb5e4a66b..9e5fd8273 100644 --- a/auth-api/src/auth_api/resources/org_utils.py +++ b/auth-api/src/auth_api/resources/org_utils.py @@ -31,6 +31,11 @@ class Result[T]: error: dict | None = None status: HTTPStatus = HTTPStatus.OK + @property + def is_success(self) -> bool: + """Return True if the result is successful.""" + return self.error is None + @property def is_failure(self) -> bool: """Return True if the result is a failure."""