From 0c9ef8700c4cc2dba7a9e59ee9d6d6ba54d8768b Mon Sep 17 00:00:00 2001 From: Justin McGuffee Date: Thu, 5 Oct 2023 12:27:55 -0500 Subject: [PATCH] Feature/11 combine profile update and fi association (#38) Closes [#11](https://github.com/cfpb/regtech-user-fi-management/issues/11) --- src/entities/models/__init__.py | 2 ++ src/entities/models/dto.py | 12 ++++++++-- src/oauth2/oauth2_admin.py | 6 ++++- src/routers/admin.py | 16 ++++++++------ tests/api/routers/test_admin_api.py | 34 ++++++++++++++++++----------- 5 files changed, 47 insertions(+), 23 deletions(-) diff --git a/src/entities/models/__init__.py b/src/entities/models/__init__.py index 3c1779c..3bd37af 100644 --- a/src/entities/models/__init__.py +++ b/src/entities/models/__init__.py @@ -9,6 +9,7 @@ "FinanicialInstitutionAssociationDto", "DeniedDomainDao", "DeniedDomainDto", + "UserProfile", "AuthenticatedUser", ] @@ -25,5 +26,6 @@ FinancialInsitutionDomainCreate, FinanicialInstitutionAssociationDto, DeniedDomainDto, + UserProfile, AuthenticatedUser, ) diff --git a/src/entities/models/dto.py b/src/entities/models/dto.py index 5214f1c..d78fd6c 100644 --- a/src/entities/models/dto.py +++ b/src/entities/models/dto.py @@ -1,5 +1,4 @@ -from typing import Any, Dict, List - +from typing import List, Dict, Any, Set, Optional from pydantic import BaseModel from starlette.authentication import BaseUser @@ -41,6 +40,15 @@ class Config: orm_mode = True +class UserProfile(BaseModel): + first_name: str + last_name: str + leis: Optional[Set[str]] + + def to_keycloak_user(self): + return {"firstName": self.first_name, "lastName": self.last_name} + + class FinanicialInstitutionAssociationDto(FinancialInstitutionDto): approved: bool diff --git a/src/oauth2/oauth2_admin.py b/src/oauth2/oauth2_admin.py index 94fb16e..ba71b10 100644 --- a/src/oauth2/oauth2_admin.py +++ b/src/oauth2/oauth2_admin.py @@ -1,7 +1,7 @@ from http import HTTPStatus import logging import os -from typing import Dict, Any +from typing import Dict, Any, Set import jose.jwt import requests @@ -88,5 +88,9 @@ def associate_to_lei(self, user_id: str, lei: str) -> None: detail="No institution found for given LEI", ) + def associate_to_leis(self, user_id: str, leis: Set[str]): + for lei in leis: + self.associate_to_lei(user_id, lei) + oauth2_admin = OAuth2Admin() diff --git a/src/routers/admin.py b/src/routers/admin.py index 816b075..744809a 100644 --- a/src/routers/admin.py +++ b/src/routers/admin.py @@ -1,9 +1,10 @@ from http import HTTPStatus -from typing import Dict, Any, Set +from typing import Set from fastapi import Depends, Request from starlette.authentication import requires from dependencies import check_domain from util import Router +from entities.models import UserProfile from entities.models import AuthenticatedUser from oauth2 import oauth2_admin @@ -13,18 +14,19 @@ @router.get("/me/", response_model=AuthenticatedUser) @requires("authenticated") -async def get_me(request: Request): +def get_me(request: Request): return request.user @router.put("/me/", status_code=HTTPStatus.ACCEPTED, dependencies=[Depends(check_domain)]) @requires("manage-account") -async def update_me(request: Request, user: Dict[str, Any]): - oauth2_admin.update_user(request.user.id, user) +def update_me(request: Request, user: UserProfile): + oauth2_admin.update_user(request.user.id, user.to_keycloak_user()) + if user.leis: + oauth2_admin.associate_to_leis(request.user.id, user.leis) @router.put("/me/institutions/", status_code=HTTPStatus.ACCEPTED, dependencies=[Depends(check_domain)]) @requires("manage-account") -async def associate_lei(request: Request, leis: Set[str]): - for lei in leis: - oauth2_admin.associate_to_lei(request.user.id, lei) +def associate_lei(request: Request, leis: Set[str]): + oauth2_admin.associate_to_leis(request.user.id, leis) diff --git a/tests/api/routers/test_admin_api.py b/tests/api/routers/test_admin_api.py index f530add..755625e 100644 --- a/tests/api/routers/test_admin_api.py +++ b/tests/api/routers/test_admin_api.py @@ -1,4 +1,4 @@ -from unittest.mock import Mock, call +from unittest.mock import Mock from fastapi import FastAPI from fastapi.testclient import TestClient @@ -40,7 +40,7 @@ def test_get_me_authed_with_institutions(self, app_fixture: FastAPI, auth_mock: def test_update_me_unauthed(self, app_fixture: FastAPI, unauthed_user_mock: Mock): client = TestClient(app_fixture) - res = client.put("/v1/admin/me", json={"firstName": "testFirst", "lastName": "testLast"}) + res = client.put("/v1/admin/me", json={"first_name": "testFirst", "last_name": "testLast", "leis": ["testLei"]}) assert res.status_code == 403 def test_update_me_no_permission(self, app_fixture: FastAPI, auth_mock: Mock): @@ -55,27 +55,35 @@ def test_update_me_no_permission(self, app_fixture: FastAPI, auth_mock: Mock): AuthenticatedUser.from_claim(claims), ) client = TestClient(app_fixture) - res = client.put("/v1/admin/me", json={"firstName": "testFirst", "lastName": "testLast"}) + res = client.put("/v1/admin/me", json={"first_name": "testFirst", "last_name": "testLast", "leis": ["testLei"]}) assert res.status_code == 403 def test_update_me(self, mocker: MockerFixture, app_fixture: FastAPI, authed_user_mock: Mock): update_user_mock = mocker.patch("oauth2.oauth2_admin.OAuth2Admin.update_user") + associate_lei_mock = mocker.patch("oauth2.oauth2_admin.OAuth2Admin.associate_to_leis") update_user_mock.return_value = None + associate_lei_mock.return_value = None client = TestClient(app_fixture) - data = {"firstName": "testFirst", "lastName": "testLast"} + data = {"first_name": "testFirst", "last_name": "testLast", "leis": ["testLei1", "testLei2"]} res = client.put("/v1/admin/me", json=data) - update_user_mock.assert_called_once_with("testuser123", data) + update_user_mock.assert_called_once_with("testuser123", {"firstName": "testFirst", "lastName": "testLast"}) + associate_lei_mock.assert_called_once_with("testuser123", {"testLei1", "testLei2"}) + assert res.status_code == 202 + + def test_update_me_no_lei(self, mocker: MockerFixture, app_fixture: FastAPI, authed_user_mock: Mock): + update_user_mock = mocker.patch("oauth2.oauth2_admin.OAuth2Admin.update_user") + associate_lei_mock = mocker.patch("oauth2.oauth2_admin.OAuth2Admin.associate_to_leis") + update_user_mock.return_value = None + client = TestClient(app_fixture) + res = client.put("/v1/admin/me", json={"first_name": "testFirst", "last_name": "testLast"}) + update_user_mock.assert_called_once_with("testuser123", {"firstName": "testFirst", "lastName": "testLast"}) + associate_lei_mock.assert_not_called() assert res.status_code == 202 def test_associate_institutions(self, mocker: MockerFixture, app_fixture: FastAPI, authed_user_mock: Mock): - associate_lei_mock = mocker.patch("oauth2.oauth2_admin.OAuth2Admin.associate_to_lei") + associate_lei_mock = mocker.patch("oauth2.oauth2_admin.OAuth2Admin.associate_to_leis") associate_lei_mock.return_value = None client = TestClient(app_fixture) - data = ["testlei1", "testlei2"] - res = client.put("/v1/admin/me/institutions", json=data) - expected_calls = [ - call("testuser123", "testlei1"), - call("testuser123", "testlei2"), - ] - associate_lei_mock.assert_has_calls(expected_calls, any_order=True) + res = client.put("/v1/admin/me/institutions", json=["testlei1", "testlei2"]) + associate_lei_mock.assert_called_once_with("testuser123", {"testlei1", "testlei2"}) assert res.status_code == 202