From e70ab22b4e332efd2c25d7d209ef148ccf14c55b Mon Sep 17 00:00:00 2001 From: Kevin Rosales Date: Sat, 1 Nov 2025 17:15:09 -0600 Subject: [PATCH] =?UTF-8?q?feat(tests):=20A=C3=B1adido=20soporte=20de=20au?= =?UTF-8?q?tenticaci=C3=B3n=20con=20access=20token=20en=20tests=20GET=20de?= =?UTF-8?q?=20todos=20los=20m=C3=B3dulos=20de=20reference=5Fdata.=20Ahora?= =?UTF-8?q?=20los=20tests=20usan=20un=20fixture=20access=5Ftoken=20y=20env?= =?UTF-8?q?=C3=ADan=20el=20header=20Authorization.=20M=C3=B3dulos=20actual?= =?UTF-8?q?izados:=20containers,=20countries,=20currency=5Ftypes,=20materi?= =?UTF-8?q?al=5Ftypes,=20states,=20valuation=5Fmethods,=20incoterms,=20pay?= =?UTF-8?q?ment=5Fmethods,=20sectors,=20customs=5Fsections,=20customs=5Fwa?= =?UTF-8?q?rehouses,=20invoice=5Ftypes,=20pedimento=5Fcodes,=20pedimento?= =?UTF-8?q?=5Fregimens,=20code=5Fpedimento=5Fregimens,=20transport=5Fmodes?= =?UTF-8?q?,=20transport=5Ftypes.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code_pedimento_regimens/dto.py | 4 +- .../code_pedimento_regimens/routes.py | 35 +++++++++++--- .../test_code_pedimento_regimens.py | 35 ++++++++++++++ .../modules/public/reference_data/conftest.py | 15 ++++++ .../public/reference_data/containers/dto.py | 4 +- .../reference_data/containers/routes.py | 41 +++++++++++++---- .../containers/test_containers.py | 35 ++++++++++++++ .../public/reference_data/countries/dto.py | 4 +- .../public/reference_data/countries/routes.py | 41 +++++++++++++---- .../countries/test_countries.py | 35 ++++++++++++++ .../reference_data/currency_types/dto.py | 4 +- .../reference_data/currency_types/routes.py | 41 +++++++++++++---- .../currency_types/test_currency_types.py | 35 ++++++++++++++ .../reference_data/customs_sections/dto.py | 4 +- .../reference_data/customs_sections/routes.py | 30 +++++++++--- .../customs_sections/test_customs_sections.py | 35 ++++++++++++++ .../reference_data/customs_warehouses/dto.py | 4 +- .../customs_warehouses/routes.py | 30 +++++++++--- .../test_customs_warehouses.py | 35 ++++++++++++++ .../public/reference_data/incoterms/dto.py | 4 +- .../public/reference_data/incoterms/routes.py | 42 +++++++++++++---- .../incoterms/test_incoterms.py | 35 ++++++++++++++ .../reference_data/invoice_types/dto.py | 4 +- .../reference_data/invoice_types/routes.py | 31 ++++++++++--- .../invoice_types/test_invoice_types.py | 35 ++++++++++++++ .../reference_data/material_types/dto.py | 4 +- .../reference_data/material_types/routes.py | 41 +++++++++++++---- .../material_types/test_material_types.py | 35 ++++++++++++++ .../reference_data/payment_methods/dto.py | 4 +- .../reference_data/payment_methods/routes.py | 30 +++++++++--- .../payment_methods/test_payment_methods.py | 35 ++++++++++++++ .../reference_data/pedimento_codes/dto.py | 4 +- .../reference_data/pedimento_codes/routes.py | 30 +++++++++--- .../pedimento_codes/test_pedimento_codes.py | 35 ++++++++++++++ .../reference_data/pedimento_regimens/dto.py | 4 +- .../pedimento_regimens/routes.py | 31 ++++++++++--- .../test_pedimento_regimens.py | 35 ++++++++++++++ .../public/reference_data/sectors/dto.py | 4 +- .../public/reference_data/sectors/routes.py | 30 +++++++++--- .../reference_data/sectors/test_sectors.py | 35 ++++++++++++++ .../public/reference_data/states/dto.py | 4 +- .../public/reference_data/states/routes.py | 44 ++++++++++++++---- .../reference_data/states/test_states.py | 35 ++++++++++++++ .../reference_data/transport_modes/dto.py | 4 +- .../reference_data/transport_modes/routes.py | 46 ++++++++++++++----- .../transport_modes/test_transport_modes.py | 35 ++++++++++++++ .../reference_data/transport_types/dto.py | 4 +- .../reference_data/transport_types/routes.py | 33 +++++++++---- .../transport_types/test_transport_types.py | 35 ++++++++++++++ .../reference_data/valuation_methods/dto.py | 4 +- .../valuation_methods/routes.py | 41 +++++++++++++---- .../test_valuation_methods.py | 35 ++++++++++++++ backend/core/database.py | 2 +- backend/core/middleware.py | 28 ++++++----- 54 files changed, 1140 insertions(+), 185 deletions(-) create mode 100644 backend/api/v1/modules/public/reference_data/code_pedimento_regimens/test_code_pedimento_regimens.py create mode 100644 backend/api/v1/modules/public/reference_data/conftest.py create mode 100644 backend/api/v1/modules/public/reference_data/containers/test_containers.py create mode 100644 backend/api/v1/modules/public/reference_data/countries/test_countries.py create mode 100644 backend/api/v1/modules/public/reference_data/currency_types/test_currency_types.py create mode 100644 backend/api/v1/modules/public/reference_data/customs_sections/test_customs_sections.py create mode 100644 backend/api/v1/modules/public/reference_data/customs_warehouses/test_customs_warehouses.py create mode 100644 backend/api/v1/modules/public/reference_data/incoterms/test_incoterms.py create mode 100644 backend/api/v1/modules/public/reference_data/invoice_types/test_invoice_types.py create mode 100644 backend/api/v1/modules/public/reference_data/material_types/test_material_types.py create mode 100644 backend/api/v1/modules/public/reference_data/payment_methods/test_payment_methods.py create mode 100644 backend/api/v1/modules/public/reference_data/pedimento_codes/test_pedimento_codes.py create mode 100644 backend/api/v1/modules/public/reference_data/pedimento_regimens/test_pedimento_regimens.py create mode 100644 backend/api/v1/modules/public/reference_data/sectors/test_sectors.py create mode 100644 backend/api/v1/modules/public/reference_data/states/test_states.py create mode 100644 backend/api/v1/modules/public/reference_data/transport_modes/test_transport_modes.py create mode 100644 backend/api/v1/modules/public/reference_data/transport_types/test_transport_types.py create mode 100644 backend/api/v1/modules/public/reference_data/valuation_methods/test_valuation_methods.py diff --git a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/dto.py b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/dto.py index fd577ffa..61d0e1fe 100644 --- a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/dto.py +++ b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/dto.py @@ -1,4 +1,5 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict from typing import Optional class CodePedimentoRegimenDTO(BaseModel): @@ -7,5 +8,4 @@ class CodePedimentoRegimenDTO(BaseModel): regimen_code: Optional[str] = Field(None, min_length=1, max_length=3) type_code: Optional[str] = Field(None, min_length=1, max_length=1) - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py index 747045fe..55672bfd 100644 --- a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py +++ b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py @@ -1,19 +1,40 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import get_current_user, has_role from .models import CodePedimentoRegimen from .dto import CodePedimentoRegimenDTO +from typing import Any, Dict + router = APIRouter(prefix="/code-pedimento-regimens", tags=["Code Pedimento Regimens"]) -@router.get("/", response_model=list[CodePedimentoRegimenDTO]) -def list_code_pedimento_regimens(db: Session = Depends(get_core_db)): - objs = db.query(CodePedimentoRegimen).all() - return [CodePedimentoRegimenDTO.model_validate(obj) for obj in objs] + +@router.get("/", response_model=Dict[str, Any]) +def list_code_pedimento_regimens( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + skip = (page - 1) * page_size + query = db.query(CodePedimentoRegimen) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [CodePedimentoRegimenDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } @router.get("/{id}", response_model=CodePedimentoRegimenDTO) -def get_code_pedimento_regimen(id: int, db: Session = Depends(get_core_db)): +def get_code_pedimento_regimen( + id: int, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): obj = db.query(CodePedimentoRegimen).filter(CodePedimentoRegimen.id == id).first() if not obj: raise HTTPException(status_code=404, detail="Not found") diff --git a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/test_code_pedimento_regimens.py b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/test_code_pedimento_regimens.py new file mode 100644 index 00000000..bd60c611 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/test_code_pedimento_regimens.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.code_pedimento_regimens.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_code_pedimento_regimens(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/code-pedimento-regimens/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_code_pedimento_regimen_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/code-pedimento-regimens/999999", headers=headers) + assert response.status_code == 404 + +def test_create_code_pedimento_regimen_forbidden(): + response = client.post("/code-pedimento-regimens/", json={"id": 999999, "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_code_pedimento_regimen_forbidden(): + response = client.put("/code-pedimento-regimens/999999", json={"id": 999999, "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_code_pedimento_regimen_forbidden(): + response = client.delete("/code-pedimento-regimens/999999") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/conftest.py b/backend/api/v1/modules/public/reference_data/conftest.py new file mode 100644 index 00000000..1d4f0fb3 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/conftest.py @@ -0,0 +1,15 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.transport_types.routes import router +from fastapi import FastAPI + +@pytest.fixture(scope="session") +def access_token(): + # Reemplaza este token por uno válido generado por Keycloak + return "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyTS1iTEZZYm9OaklYM3NjaVlmODNGZGVRY2s3ZzI3cmxZckowNzRCMGVFIn0.eyJleHAiOjE3NjIwNzM2ODksImlhdCI6MTc2MjAzNzY4OSwianRpIjoib25ydHJvOmM5OGM2M2I2LTcxMWUtNWUxYS0wZmE4LTFjNTMwN2VlNTY5NSIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9yZWFsbXMvbWFzdGVyIiwiYXVkIjpbIm1hc3Rlci1yZWFsbSIsImFjY291bnQiXSwic3ViIjoiNzVmM2E0MzQtODM2MC00NTY0LTk0NmMtMGE3ZTFkNWI3MzMwIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYW5leG83Ni1iYWNrZW5kIiwic2lkIjoiYTYwMzdkZjQtMGIzYS02MGZjLTU4NmQtYTc1NWMwMzc5YjAxIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjgwMDAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImNyZWF0ZS1yZWFsbSIsImRlZmF1bHQtcm9sZXMtbWFzdGVyIiwib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdfSwicmVzb3VyY2VfYWNjZXNzIjp7Im1hc3Rlci1yZWFsbSI6eyJyb2xlcyI6WyJ2aWV3LXJlYWxtIiwidmlldy1pZGVudGl0eS1wcm92aWRlcnMiLCJtYW5hZ2UtaWRlbnRpdHktcHJvdmlkZXJzIiwiaW1wZXJzb25hdGlvbiIsImNyZWF0ZS1jbGllbnQiLCJtYW5hZ2UtdXNlcnMiLCJxdWVyeS1yZWFsbXMiLCJ2aWV3LWF1dGhvcml6YXRpb24iLCJxdWVyeS1jbGllbnRzIiwicXVlcnktdXNlcnMiLCJtYW5hZ2UtZXZlbnRzIiwibWFuYWdlLXJlYWxtIiwidmlldy1ldmVudHMiLCJ2aWV3LXVzZXJzIiwidmlldy1jbGllbnRzIiwibWFuYWdlLWF1dGhvcml6YXRpb24iLCJtYW5hZ2UtY2xpZW50cyIsInF1ZXJ5LWdyb3VwcyJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSIsInRlbmFudF9pZCI6IjEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJLZXZpbiBSb3NhbGVzIiwicHJlZmVycmVkX3VzZXJuYW1lIjoia3Jvc2FsZXMiLCJnaXZlbl9uYW1lIjoiS2V2aW4iLCJmYW1pbHlfbmFtZSI6IlJvc2FsZXMiLCJlbWFpbCI6Imtyb3NhbGVzQGFkdWFuYXNvZnQuY29tLm14In0.NgofV0pK3tiLN-hyWechIIG67QPIfJDK3fZvLkBuhhgExlQNcargt3zK6dn3JIzPPE3hpmaVBZmmARRTNB7v9xgP3Hg8tXsnJMPVU4gStBM8-XDn0vYNEEehAemp-F2xLNlbCmZtOtBk2ZF-lLecSa67yQ6VXJJv1_1K420w7KSUUU4nxXobAcXmYgJSb9d8O5zJekDRlDSp8TQCa25qOZTqUbjdpVnZh_sdMpzPxnFGDqxQd99_erQMPGG0Mu5CuhH9V8EIin9BdkjWWtafc6q64qmig8yNVJIJ0I3Mul01Bo0A2EZXIQhTaDxsGf8GiAIEYDjUZSl-5u5EYck7lg" + +@pytest.fixture(scope="session") +def client(): + app = FastAPI() + app.include_router(router) + return TestClient(app) diff --git a/backend/api/v1/modules/public/reference_data/containers/dto.py b/backend/api/v1/modules/public/reference_data/containers/dto.py index aa82cd78..baf3f921 100644 --- a/backend/api/v1/modules/public/reference_data/containers/dto.py +++ b/backend/api/v1/modules/public/reference_data/containers/dto.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict class ContainerDTO(BaseModel): key: str = Field(..., min_length=1, max_length=3) description: str - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/containers/routes.py b/backend/api/v1/modules/public/reference_data/containers/routes.py index 13c8a147..83a52e1a 100644 --- a/backend/api/v1/modules/public/reference_data/containers/routes.py +++ b/backend/api/v1/modules/public/reference_data/containers/routes.py @@ -1,25 +1,46 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import get_current_user, has_role from .models import Container from .dto import ContainerDTO +from typing import Any, Dict + router = APIRouter(prefix="/containers", tags=["Containers"]) -@router.get("/", response_model=list[ContainerDTO]) -def list_containers(db: Session = Depends(get_core_db)): - return db.query(Container).all() + +@router.get("/", response_model=Dict[str, Any]) +async def list_containers( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + skip = (page - 1) * page_size + query = db.query(Container) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [ContainerDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } + + @router.get("/{key}", response_model=ContainerDTO) -def get_container(key: str, db: Session = Depends(get_core_db)): +async def get_container(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): obj = db.query(Container).filter(Container.key == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj + @router.post("/", response_model=ContainerDTO, status_code=201) -def create_container( +async def create_container( data: ContainerDTO, db: Session = Depends(get_core_db), current_user: dict = Depends(has_role("admin")) @@ -30,8 +51,9 @@ def create_container( db.refresh(obj) return obj + @router.put("/{key}", response_model=ContainerDTO) -def update_container( +async def update_container( key: str, data: ContainerDTO, db: Session = Depends(get_core_db), @@ -46,8 +68,9 @@ def update_container( db.refresh(obj) return obj + @router.delete("/{key}", status_code=204) -def delete_container( +async def delete_container( key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(has_role("admin")) diff --git a/backend/api/v1/modules/public/reference_data/containers/test_containers.py b/backend/api/v1/modules/public/reference_data/containers/test_containers.py new file mode 100644 index 00000000..392dd06e --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/containers/test_containers.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.containers.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_containers(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/containers/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_container_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/containers/invalid_key", headers=headers) + assert response.status_code == 404 + +def test_create_container_forbidden(): + response = client.post("/containers/", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_container_forbidden(): + response = client.put("/containers/TST", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_container_forbidden(): + response = client.delete("/containers/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/countries/dto.py b/backend/api/v1/modules/public/reference_data/countries/dto.py index 0a261ca5..6762b39a 100644 --- a/backend/api/v1/modules/public/reference_data/countries/dto.py +++ b/backend/api/v1/modules/public/reference_data/countries/dto.py @@ -1,4 +1,5 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict class CountryDTO(BaseModel): m3_key: str = Field(..., min_length=1, max_length=3) @@ -7,6 +8,5 @@ class CountryDTO(BaseModel): description_es: str description_en: str - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/countries/routes.py b/backend/api/v1/modules/public/reference_data/countries/routes.py index 7282e6b7..950140f3 100644 --- a/backend/api/v1/modules/public/reference_data/countries/routes.py +++ b/backend/api/v1/modules/public/reference_data/countries/routes.py @@ -1,25 +1,46 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import get_current_user, has_role from .models import Country from .dto import CountryDTO +from typing import Any, Dict + router = APIRouter(prefix="/countries", tags=["Countries"]) -@router.get("/", response_model=list[CountryDTO]) -def list_countries(db: Session = Depends(get_core_db)): - return db.query(Country).all() + +@router.get("/", response_model=Dict[str, Any]) +async def list_countries( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + skip = (page - 1) * page_size + query = db.query(Country) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [CountryDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } + + @router.get("/{m3_key}", response_model=CountryDTO) -def get_country(m3_key: str, db: Session = Depends(get_core_db)): +async def get_country(m3_key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): obj = db.query(Country).filter(Country.m3_key == m3_key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj + @router.post("/", response_model=CountryDTO, status_code=201) -def create_country( +async def create_country( data: CountryDTO, db: Session = Depends(get_core_db), current_user: dict = Depends(has_role("admin")) @@ -30,8 +51,9 @@ def create_country( db.refresh(obj) return obj + @router.put("/{m3_key}", response_model=CountryDTO) -def update_country( +async def update_country( m3_key: str, data: CountryDTO, db: Session = Depends(get_core_db), @@ -46,8 +68,9 @@ def update_country( db.refresh(obj) return obj + @router.delete("/{m3_key}", status_code=204) -def delete_country( +async def delete_country( m3_key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(has_role("admin")) diff --git a/backend/api/v1/modules/public/reference_data/countries/test_countries.py b/backend/api/v1/modules/public/reference_data/countries/test_countries.py new file mode 100644 index 00000000..b0f3af67 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/countries/test_countries.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.countries.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_countries(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/countries/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_country_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/countries/invalid_key", headers=headers) + assert response.status_code == 404 + +def test_create_country_forbidden(): + response = client.post("/countries/", json={"m3_key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_country_forbidden(): + response = client.put("/countries/TST", json={"m3_key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_country_forbidden(): + response = client.delete("/countries/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/currency_types/dto.py b/backend/api/v1/modules/public/reference_data/currency_types/dto.py index 1e1cae4f..6e9f108f 100644 --- a/backend/api/v1/modules/public/reference_data/currency_types/dto.py +++ b/backend/api/v1/modules/public/reference_data/currency_types/dto.py @@ -1,10 +1,10 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict class CurrencyTypeDTO(BaseModel): code: str = Field(..., min_length=1, max_length=3) currency_name: str country_description: str - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/currency_types/routes.py b/backend/api/v1/modules/public/reference_data/currency_types/routes.py index 627c2fe9..5987808f 100644 --- a/backend/api/v1/modules/public/reference_data/currency_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/currency_types/routes.py @@ -1,25 +1,46 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import get_current_user, has_role from .models import CurrencyType from .dto import CurrencyTypeDTO +from typing import Any, Dict + router = APIRouter(prefix="/currency-types", tags=["Currency Types"]) -@router.get("/", response_model=list[CurrencyTypeDTO]) -def list_currency_types(db: Session = Depends(get_core_db)): - return db.query(CurrencyType).all() + +@router.get("/", response_model=Dict[str, Any]) +async def list_currency_types( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + skip = (page - 1) * page_size + query = db.query(CurrencyType) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [CurrencyTypeDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } + + @router.get("/{code}", response_model=CurrencyTypeDTO) -def get_currency_type(code: str, db: Session = Depends(get_core_db)): +async def get_currency_type(code: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): obj = db.query(CurrencyType).filter(CurrencyType.code == code).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj + @router.post("/", response_model=CurrencyTypeDTO, status_code=201) -def create_currency_type( +async def create_currency_type( data: CurrencyTypeDTO, db: Session = Depends(get_core_db), current_user: dict = Depends(has_role("admin")) @@ -30,8 +51,9 @@ def create_currency_type( db.refresh(obj) return obj + @router.put("/{code}", response_model=CurrencyTypeDTO) -def update_currency_type( +async def update_currency_type( code: str, data: CurrencyTypeDTO, db: Session = Depends(get_core_db), @@ -46,8 +68,9 @@ def update_currency_type( db.refresh(obj) return obj + @router.delete("/{code}", status_code=204) -def delete_currency_type( +async def delete_currency_type( code: str, db: Session = Depends(get_core_db), current_user: dict = Depends(has_role("admin")) diff --git a/backend/api/v1/modules/public/reference_data/currency_types/test_currency_types.py b/backend/api/v1/modules/public/reference_data/currency_types/test_currency_types.py new file mode 100644 index 00000000..7123dc3c --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/currency_types/test_currency_types.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.currency_types.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_currency_types(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/currency-types/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_currency_type_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/currency-types/invalid_code", headers=headers) + assert response.status_code == 404 + +def test_create_currency_type_forbidden(): + response = client.post("/currency-types/", json={"code": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_currency_type_forbidden(): + response = client.put("/currency-types/TST", json={"code": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_currency_type_forbidden(): + response = client.delete("/currency-types/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/customs_sections/dto.py b/backend/api/v1/modules/public/reference_data/customs_sections/dto.py index c317ccd6..2bc56f20 100644 --- a/backend/api/v1/modules/public/reference_data/customs_sections/dto.py +++ b/backend/api/v1/modules/public/reference_data/customs_sections/dto.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict class CustomsSectionDTO(BaseModel): customs_code: str = Field(..., min_length=1, max_length=3) section_name: str - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/customs_sections/routes.py b/backend/api/v1/modules/public/reference_data/customs_sections/routes.py index 403e8904..70225245 100644 --- a/backend/api/v1/modules/public/reference_data/customs_sections/routes.py +++ b/backend/api/v1/modules/public/reference_data/customs_sections/routes.py @@ -1,18 +1,36 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import get_current_user, has_role from .models import CustomsSection from .dto import CustomsSectionDTO +from typing import Any, Dict + router = APIRouter(prefix="/customs-sections", tags=["Customs Sections"]) -@router.get("/", response_model=list[CustomsSectionDTO]) -def list_customs_sections(db: Session = Depends(get_core_db)): - return db.query(CustomsSection).all() + +@router.get("/", response_model=Dict[str, Any]) +def list_customs_sections( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + skip = (page - 1) * page_size + query = db.query(CustomsSection) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [CustomsSectionDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } @router.get("/{customs_code}", response_model=CustomsSectionDTO) -def get_customs_section(customs_code: str, db: Session = Depends(get_core_db)): +def get_customs_section(customs_code: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): obj = db.query(CustomsSection).filter(CustomsSection.customs_code == customs_code).first() if not obj: raise HTTPException(status_code=404, detail="Not found") diff --git a/backend/api/v1/modules/public/reference_data/customs_sections/test_customs_sections.py b/backend/api/v1/modules/public/reference_data/customs_sections/test_customs_sections.py new file mode 100644 index 00000000..2b000f46 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/customs_sections/test_customs_sections.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.customs_sections.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_customs_sections(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/customs-sections/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_customs_section_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/customs-sections/invalid_code", headers=headers) + assert response.status_code == 404 + +def test_create_customs_section_forbidden(): + response = client.post("/customs-sections/", json={"customs_code": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_customs_section_forbidden(): + response = client.put("/customs-sections/TST", json={"customs_code": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_customs_section_forbidden(): + response = client.delete("/customs-sections/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/customs_warehouses/dto.py b/backend/api/v1/modules/public/reference_data/customs_warehouses/dto.py index bbf0f1c7..17711567 100644 --- a/backend/api/v1/modules/public/reference_data/customs_warehouses/dto.py +++ b/backend/api/v1/modules/public/reference_data/customs_warehouses/dto.py @@ -1,10 +1,10 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict class CustomsWarehouseDTO(BaseModel): key: str = Field(..., min_length=1, max_length=3) customs: str fiscalized_warehouse: str - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py b/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py index 417faf33..9c0ca9f0 100644 --- a/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py +++ b/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py @@ -1,18 +1,36 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import get_current_user, has_role from .models import CustomsWarehouse from .dto import CustomsWarehouseDTO +from typing import Any, Dict + router = APIRouter(prefix="/customs-warehouses", tags=["Customs Warehouses"]) -@router.get("/", response_model=list[CustomsWarehouseDTO]) -def list_customs_warehouses(db: Session = Depends(get_core_db)): - return db.query(CustomsWarehouse).all() + +@router.get("/", response_model=Dict[str, Any]) +def list_customs_warehouses( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + skip = (page - 1) * page_size + query = db.query(CustomsWarehouse) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [CustomsWarehouseDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } @router.get("/{key}/{customs}", response_model=CustomsWarehouseDTO) -def get_customs_warehouse(key: str, customs: str, db: Session = Depends(get_core_db)): +def get_customs_warehouse(key: str, customs: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): obj = db.query(CustomsWarehouse).filter(CustomsWarehouse.key == key, CustomsWarehouse.customs == customs).first() if not obj: raise HTTPException(status_code=404, detail="Not found") diff --git a/backend/api/v1/modules/public/reference_data/customs_warehouses/test_customs_warehouses.py b/backend/api/v1/modules/public/reference_data/customs_warehouses/test_customs_warehouses.py new file mode 100644 index 00000000..315929c1 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/customs_warehouses/test_customs_warehouses.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.customs_warehouses.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_customs_warehouses(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/customs-warehouses/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_customs_warehouse_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/customs-warehouses/invalid_key/invalid_customs", headers=headers) + assert response.status_code == 404 + +def test_create_customs_warehouse_forbidden(): + response = client.post("/customs-warehouses/", json={"key": "TST", "customs": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_customs_warehouse_forbidden(): + response = client.put("/customs-warehouses/TST/TST", json={"key": "TST", "customs": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_customs_warehouse_forbidden(): + response = client.delete("/customs-warehouses/TST/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/incoterms/dto.py b/backend/api/v1/modules/public/reference_data/incoterms/dto.py index 963816e9..0ff49602 100644 --- a/backend/api/v1/modules/public/reference_data/incoterms/dto.py +++ b/backend/api/v1/modules/public/reference_data/incoterms/dto.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict class IncotermDTO(BaseModel): code: str = Field(..., min_length=1, max_length=5) description_es: str description_en: str - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/incoterms/routes.py b/backend/api/v1/modules/public/reference_data/incoterms/routes.py index b873cba8..3bc6ea7b 100644 --- a/backend/api/v1/modules/public/reference_data/incoterms/routes.py +++ b/backend/api/v1/modules/public/reference_data/incoterms/routes.py @@ -1,26 +1,46 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import get_current_user, has_role from .models import Incoterm from .dto import IncotermDTO +from typing import Any, Dict + router = APIRouter(prefix="/incoterms", tags=["Incoterms"]) -@router.get("/", response_model=list[IncotermDTO]) -def list_incoterms(db: Session = Depends(get_core_db)): - objs = db.query(Incoterm).all() - return [IncotermDTO.model_validate(obj) for obj in objs] + +@router.get("/", response_model=Dict[str, Any]) +async def list_incoterms( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + skip = (page - 1) * page_size + query = db.query(Incoterm) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [IncotermDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } + + @router.get("/{key}", response_model=IncotermDTO) -def get_incoterm(key: str, db: Session = Depends(get_core_db)): +async def get_incoterm(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): obj = db.query(Incoterm).filter(Incoterm.code == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return IncotermDTO.model_validate(obj) + @router.post("/", response_model=IncotermDTO, status_code=201) -def create_incoterm( +async def create_incoterm( data: IncotermDTO, db: Session = Depends(get_core_db), current_user: dict = Depends(has_role("admin")) @@ -31,8 +51,9 @@ def create_incoterm( db.refresh(obj) return IncotermDTO.model_validate(obj) + @router.put("/{key}", response_model=IncotermDTO) -def update_incoterm( +async def update_incoterm( key: str, data: IncotermDTO, db: Session = Depends(get_core_db), @@ -47,8 +68,9 @@ def update_incoterm( db.refresh(obj) return IncotermDTO.model_validate(obj) + @router.delete("/{key}", status_code=204) -def delete_incoterm( +async def delete_incoterm( key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(has_role("admin")) diff --git a/backend/api/v1/modules/public/reference_data/incoterms/test_incoterms.py b/backend/api/v1/modules/public/reference_data/incoterms/test_incoterms.py new file mode 100644 index 00000000..865744d1 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/incoterms/test_incoterms.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.incoterms.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_incoterms(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/incoterms/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_incoterm_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/incoterms/invalid_key", headers=headers) + assert response.status_code == 404 + +def test_create_incoterm_forbidden(): + response = client.post("/incoterms/", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_incoterm_forbidden(): + response = client.put("/incoterms/TST", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_incoterm_forbidden(): + response = client.delete("/incoterms/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/dto.py b/backend/api/v1/modules/public/reference_data/invoice_types/dto.py index a71b6a88..3e3cd433 100644 --- a/backend/api/v1/modules/public/reference_data/invoice_types/dto.py +++ b/backend/api/v1/modules/public/reference_data/invoice_types/dto.py @@ -1,4 +1,5 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict from typing import Optional class InvoiceTypeDTO(BaseModel): @@ -7,5 +8,4 @@ class InvoiceTypeDTO(BaseModel): note: Optional[str] = None type: Optional[str] = None - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/routes.py b/backend/api/v1/modules/public/reference_data/invoice_types/routes.py index 01bdda44..eddcf8e3 100644 --- a/backend/api/v1/modules/public/reference_data/invoice_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/invoice_types/routes.py @@ -1,19 +1,36 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import get_current_user, has_role from .models import InvoiceType from .dto import InvoiceTypeDTO +from typing import Any, Dict + router = APIRouter(prefix="/invoice-types", tags=["Invoice Types"]) -@router.get("/", response_model=list[InvoiceTypeDTO]) -def list_invoice_types(db: Session = Depends(get_core_db)): - objs = db.query(InvoiceType).all() - return [InvoiceTypeDTO.model_validate(obj) for obj in objs] + +@router.get("/", response_model=Dict[str, Any]) +def list_invoice_types( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + skip = (page - 1) * page_size + query = db.query(InvoiceType) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [InvoiceTypeDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } @router.get("/{key}", response_model=InvoiceTypeDTO) -def get_invoice_type(key: str, db: Session = Depends(get_core_db)): +def get_invoice_type(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): obj = db.query(InvoiceType).filter(InvoiceType.key == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/test_invoice_types.py b/backend/api/v1/modules/public/reference_data/invoice_types/test_invoice_types.py new file mode 100644 index 00000000..1f2d2897 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/invoice_types/test_invoice_types.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.invoice_types.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_invoice_types(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/invoice-types/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_invoice_type_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/invoice-types/invalid_key", headers=headers) + assert response.status_code == 404 + +def test_create_invoice_type_forbidden(): + response = client.post("/invoice-types/", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_invoice_type_forbidden(): + response = client.put("/invoice-types/TST", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_invoice_type_forbidden(): + response = client.delete("/invoice-types/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/material_types/dto.py b/backend/api/v1/modules/public/reference_data/material_types/dto.py index 22c83fa5..f04e0d46 100644 --- a/backend/api/v1/modules/public/reference_data/material_types/dto.py +++ b/backend/api/v1/modules/public/reference_data/material_types/dto.py @@ -1,10 +1,10 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict class MaterialTypeDTO(BaseModel): key: str = Field(..., min_length=1, max_length=10) type: str = Field(..., min_length=1, max_length=15) description: str - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/material_types/routes.py b/backend/api/v1/modules/public/reference_data/material_types/routes.py index 367b2cae..da83e152 100644 --- a/backend/api/v1/modules/public/reference_data/material_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/material_types/routes.py @@ -1,25 +1,46 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import get_current_user, has_role from .models import MaterialType from .dto import MaterialTypeDTO +from typing import Any, Dict + router = APIRouter(prefix="/material-types", tags=["Material Types"]) -@router.get("/", response_model=list[MaterialTypeDTO]) -def list_material_types(db: Session = Depends(get_core_db)): - return db.query(MaterialType).all() + +@router.get("/", response_model=Dict[str, Any]) +async def list_material_types( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + skip = (page - 1) * page_size + query = db.query(MaterialType) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [MaterialTypeDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } + + @router.get("/{key}", response_model=MaterialTypeDTO) -def get_material_type(key: str, db: Session = Depends(get_core_db)): +async def get_material_type(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): obj = db.query(MaterialType).filter(MaterialType.key == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj + @router.post("/", response_model=MaterialTypeDTO, status_code=201) -def create_material_type( +async def create_material_type( data: MaterialTypeDTO, db: Session = Depends(get_core_db), current_user: dict = Depends(has_role("admin")) @@ -30,8 +51,9 @@ def create_material_type( db.refresh(obj) return obj + @router.put("/{key}", response_model=MaterialTypeDTO) -def update_material_type( +async def update_material_type( key: str, data: MaterialTypeDTO, db: Session = Depends(get_core_db), @@ -46,8 +68,9 @@ def update_material_type( db.refresh(obj) return obj + @router.delete("/{key}", status_code=204) -def delete_material_type( +async def delete_material_type( key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(has_role("admin")) diff --git a/backend/api/v1/modules/public/reference_data/material_types/test_material_types.py b/backend/api/v1/modules/public/reference_data/material_types/test_material_types.py new file mode 100644 index 00000000..796b1971 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/material_types/test_material_types.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.material_types.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_material_types(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/material-types/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_material_type_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/material-types/invalid_key", headers=headers) + assert response.status_code == 404 + +def test_create_material_type_forbidden(): + response = client.post("/material-types/", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_material_type_forbidden(): + response = client.put("/material-types/TST", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_material_type_forbidden(): + response = client.delete("/material-types/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/payment_methods/dto.py b/backend/api/v1/modules/public/reference_data/payment_methods/dto.py index 214a2087..c98ff8b9 100644 --- a/backend/api/v1/modules/public/reference_data/payment_methods/dto.py +++ b/backend/api/v1/modules/public/reference_data/payment_methods/dto.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict class PaymentMethodDTO(BaseModel): key: str = Field(..., min_length=1, max_length=2) description: str - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/payment_methods/routes.py b/backend/api/v1/modules/public/reference_data/payment_methods/routes.py index 3e989198..104de6b8 100644 --- a/backend/api/v1/modules/public/reference_data/payment_methods/routes.py +++ b/backend/api/v1/modules/public/reference_data/payment_methods/routes.py @@ -1,18 +1,36 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import get_current_user, has_role from .models import PaymentMethod from .dto import PaymentMethodDTO +from typing import Any, Dict + router = APIRouter(prefix="/payment-methods", tags=["Payment Methods"]) -@router.get("/", response_model=list[PaymentMethodDTO]) -def list_payment_methods(db: Session = Depends(get_core_db)): - return db.query(PaymentMethod).all() + +@router.get("/", response_model=Dict[str, Any]) +def list_payment_methods( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + skip = (page - 1) * page_size + query = db.query(PaymentMethod) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [PaymentMethodDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } @router.get("/{key}", response_model=PaymentMethodDTO) -def get_payment_method(key: str, db: Session = Depends(get_core_db)): +def get_payment_method(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): obj = db.query(PaymentMethod).filter(PaymentMethod.key == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") diff --git a/backend/api/v1/modules/public/reference_data/payment_methods/test_payment_methods.py b/backend/api/v1/modules/public/reference_data/payment_methods/test_payment_methods.py new file mode 100644 index 00000000..a16785a1 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/payment_methods/test_payment_methods.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.payment_methods.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_payment_methods(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/payment-methods/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_payment_method_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/payment-methods/invalid_key", headers=headers) + assert response.status_code == 404 + +def test_create_payment_method_forbidden(): + response = client.post("/payment-methods/", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_payment_method_forbidden(): + response = client.put("/payment-methods/TST", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_payment_method_forbidden(): + response = client.delete("/payment-methods/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/pedimento_codes/dto.py b/backend/api/v1/modules/public/reference_data/pedimento_codes/dto.py index f44cb77b..20a1053f 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_codes/dto.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_codes/dto.py @@ -1,10 +1,10 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict from typing import Optional class PedimentoCodeDTO(BaseModel): code: str = Field(..., min_length=1, max_length=3) description: str - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py b/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py index 954a566b..d00ac957 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py @@ -1,18 +1,36 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import get_current_user, has_role from .models import PedimentoCode from .dto import PedimentoCodeDTO +from typing import Any, Dict + router = APIRouter(prefix="/pedimento-codes", tags=["Pedimento Codes"]) -@router.get("/", response_model=list[PedimentoCodeDTO]) -def list_pedimento_codes(db: Session = Depends(get_core_db)): - return db.query(PedimentoCode).all() + +@router.get("/", response_model=Dict[str, Any]) +def list_pedimento_codes( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + skip = (page - 1) * page_size + query = db.query(PedimentoCode) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [PedimentoCodeDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } @router.get("/{code}", response_model=PedimentoCodeDTO) -def get_pedimento_code(code: str, db: Session = Depends(get_core_db)): +def get_pedimento_code(code: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): obj = db.query(PedimentoCode).filter(PedimentoCode.code == code).first() if not obj: raise HTTPException(status_code=404, detail="Not found") diff --git a/backend/api/v1/modules/public/reference_data/pedimento_codes/test_pedimento_codes.py b/backend/api/v1/modules/public/reference_data/pedimento_codes/test_pedimento_codes.py new file mode 100644 index 00000000..92f9ebc4 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/pedimento_codes/test_pedimento_codes.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.pedimento_codes.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_pedimento_codes(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/pedimento-codes/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_pedimento_code_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/pedimento-codes/invalid_code", headers=headers) + assert response.status_code == 404 + +def test_create_pedimento_code_forbidden(): + response = client.post("/pedimento-codes/", json={"code": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_pedimento_code_forbidden(): + response = client.put("/pedimento-codes/TST", json={"code": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_pedimento_code_forbidden(): + response = client.delete("/pedimento-codes/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/pedimento_regimens/dto.py b/backend/api/v1/modules/public/reference_data/pedimento_regimens/dto.py index 578aef9c..3a0673d8 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_regimens/dto.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_regimens/dto.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict from typing import List class RegimenPedimentoDTO(BaseModel): code: str = Field(..., min_length=1, max_length=3) description: str - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py b/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py index cf435fe7..a3cb398a 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py @@ -1,19 +1,36 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import get_current_user, has_role from .models import RegimenPedimento from .dto import RegimenPedimentoDTO +from typing import Any, Dict + router = APIRouter(prefix="/pedimento-regimens", tags=["Pedimento Regimens"]) -@router.get("/", response_model=list[RegimenPedimentoDTO]) -def list_pedimento_regimens(db: Session = Depends(get_core_db)): - objs = db.query(RegimenPedimento).all() - return [RegimenPedimentoDTO.model_validate(obj) for obj in objs] + +@router.get("/", response_model=Dict[str, Any]) +def list_pedimento_regimens( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + skip = (page - 1) * page_size + query = db.query(RegimenPedimento) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [RegimenPedimentoDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } @router.get("/{key}", response_model=RegimenPedimentoDTO) -def get_pedimento_regimen(key: str, db: Session = Depends(get_core_db)): +def get_pedimento_regimen(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): obj = db.query(RegimenPedimento).filter(RegimenPedimento.code == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") diff --git a/backend/api/v1/modules/public/reference_data/pedimento_regimens/test_pedimento_regimens.py b/backend/api/v1/modules/public/reference_data/pedimento_regimens/test_pedimento_regimens.py new file mode 100644 index 00000000..b1ceaf54 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/pedimento_regimens/test_pedimento_regimens.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.pedimento_regimens.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_pedimento_regimens(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/pedimento-regimens/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_pedimento_regimen_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/pedimento-regimens/invalid_key", headers=headers) + assert response.status_code == 404 + +def test_create_pedimento_regimen_forbidden(): + response = client.post("/pedimento-regimens/", json={"code": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_pedimento_regimen_forbidden(): + response = client.put("/pedimento-regimens/TST", json={"code": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_pedimento_regimen_forbidden(): + response = client.delete("/pedimento-regimens/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/sectors/dto.py b/backend/api/v1/modules/public/reference_data/sectors/dto.py index 0bfd002d..2f92ae9d 100644 --- a/backend/api/v1/modules/public/reference_data/sectors/dto.py +++ b/backend/api/v1/modules/public/reference_data/sectors/dto.py @@ -1,10 +1,10 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict class SectorDTO(BaseModel): key: str = Field(..., min_length=1, max_length=8) description: str authorized: int - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/sectors/routes.py b/backend/api/v1/modules/public/reference_data/sectors/routes.py index 59e21912..dd4b5694 100644 --- a/backend/api/v1/modules/public/reference_data/sectors/routes.py +++ b/backend/api/v1/modules/public/reference_data/sectors/routes.py @@ -1,18 +1,36 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import get_current_user, has_role from .models import Sector from .dto import SectorDTO +from typing import Any, Dict + router = APIRouter(prefix="/sectors", tags=["Sectors"]) -@router.get("/", response_model=list[SectorDTO]) -def list_sectors(db: Session = Depends(get_core_db)): - return db.query(Sector).all() + +@router.get("/", response_model=Dict[str, Any]) +def list_sectors( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + skip = (page - 1) * page_size + query = db.query(Sector) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [SectorDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } @router.get("/{key}", response_model=SectorDTO) -def get_sector(key: str, db: Session = Depends(get_core_db)): +def get_sector(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): obj = db.query(Sector).filter(Sector.key == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") diff --git a/backend/api/v1/modules/public/reference_data/sectors/test_sectors.py b/backend/api/v1/modules/public/reference_data/sectors/test_sectors.py new file mode 100644 index 00000000..2a59d667 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/sectors/test_sectors.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.sectors.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_sectors(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/sectors/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_sector_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/sectors/invalid_key", headers=headers) + assert response.status_code == 404 + +def test_create_sector_forbidden(): + response = client.post("/sectors/", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_sector_forbidden(): + response = client.put("/sectors/TST", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_sector_forbidden(): + response = client.delete("/sectors/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/states/dto.py b/backend/api/v1/modules/public/reference_data/states/dto.py index 8070c740..b4a8e219 100644 --- a/backend/api/v1/modules/public/reference_data/states/dto.py +++ b/backend/api/v1/modules/public/reference_data/states/dto.py @@ -1,4 +1,5 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict from typing import Optional class StateDTO(BaseModel): @@ -7,6 +8,5 @@ class StateDTO(BaseModel): mex_key: Optional[str] = None ame_key: Optional[str] = None - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/states/routes.py b/backend/api/v1/modules/public/reference_data/states/routes.py index 1835c9d9..e63a8fd0 100644 --- a/backend/api/v1/modules/public/reference_data/states/routes.py +++ b/backend/api/v1/modules/public/reference_data/states/routes.py @@ -1,42 +1,66 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import get_current_user, has_role from .models import State from .dto import StateDTO +from typing import Any, Dict + router = APIRouter(prefix="/states", tags=["States"]) -@router.get("/", response_model=list[StateDTO]) -def list_states(db: Session = Depends(get_core_db)): - return db.query(State).all() + +@router.get("/", response_model=Dict[str, Any]) +async def list_states( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + skip = (page - 1) * page_size + query = db.query(State) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [StateDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } + + @router.get("/{m3_key}", response_model=StateDTO) -def get_state(m3_key: str, db: Session = Depends(get_core_db)): +async def get_state(m3_key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): obj = db.query(State).filter(State.m3_key == m3_key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj + @router.post("/", response_model=StateDTO, status_code=201) -def create_state( +async def create_state( data: StateDTO, db: Session = Depends(get_core_db), current_user: dict = Depends(has_role("admin")) ): + obj = State(**data.dict()) db.add(obj) db.commit() db.refresh(obj) return obj + @router.put("/{m3_key}", response_model=StateDTO) -def update_state( +async def update_state( m3_key: str, data: StateDTO, db: Session = Depends(get_core_db), current_user: dict = Depends(has_role("admin")) ): + obj = db.query(State).filter(State.m3_key == m3_key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") @@ -46,12 +70,14 @@ def update_state( db.refresh(obj) return obj + @router.delete("/{m3_key}", status_code=204) -def delete_state( +async def delete_state( m3_key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(has_role("admin")) ): + obj = db.query(State).filter(State.m3_key == m3_key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") diff --git a/backend/api/v1/modules/public/reference_data/states/test_states.py b/backend/api/v1/modules/public/reference_data/states/test_states.py new file mode 100644 index 00000000..27003749 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/states/test_states.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.states.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_states(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/states/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_state_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/states/invalid_key", headers=headers) + assert response.status_code == 404 + +def test_create_state_forbidden(): + response = client.post("/states/", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_state_forbidden(): + response = client.put("/states/TST", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_state_forbidden(): + response = client.delete("/states/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/transport_modes/dto.py b/backend/api/v1/modules/public/reference_data/transport_modes/dto.py index 5b85d32b..5ba8b9c4 100644 --- a/backend/api/v1/modules/public/reference_data/transport_modes/dto.py +++ b/backend/api/v1/modules/public/reference_data/transport_modes/dto.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict class TransportModeDTO(BaseModel): key: str = Field(..., min_length=1, max_length=3) name: str - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/transport_modes/routes.py b/backend/api/v1/modules/public/reference_data/transport_modes/routes.py index 224dc779..883b8763 100644 --- a/backend/api/v1/modules/public/reference_data/transport_modes/routes.py +++ b/backend/api/v1/modules/public/reference_data/transport_modes/routes.py @@ -1,28 +1,48 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import has_role, get_current_user from .models import TransportMode from .dto import TransportModeDTO +from typing import Any, Dict + router = APIRouter(prefix="/transport-modes", tags=["Transport Modes"]) -@router.get("/", response_model=list[TransportModeDTO]) -def list_transport_modes(db: Session = Depends(get_core_db)): - return db.query(TransportMode).all() + +@router.get("/", response_model=Dict[str, Any]) +async def list_transport_modes( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db) +): + skip = (page - 1) * page_size + query = db.query(TransportMode) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [TransportModeDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } + + @router.get("/{key}", response_model=TransportModeDTO) -def get_transport_mode(key: str, db: Session = Depends(get_core_db)): +async def get_transport_mode(key: str, db: Session = Depends(get_core_db)): obj = db.query(TransportMode).filter(TransportMode.key == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj + @router.post("/", response_model=TransportModeDTO, status_code=201) -def create_transport_mode( +async def create_transport_mode( data: TransportModeDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + user=Depends(get_current_user) ): obj = TransportMode(**data.dict()) db.add(obj) @@ -30,12 +50,13 @@ def create_transport_mode( db.refresh(obj) return obj + @router.put("/{key}", response_model=TransportModeDTO) -def update_transport_mode( +async def update_transport_mode( key: str, data: TransportModeDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + user=Depends(get_current_user) ): obj = db.query(TransportMode).filter(TransportMode.key == key).first() if not obj: @@ -46,11 +67,12 @@ def update_transport_mode( db.refresh(obj) return obj + @router.delete("/{key}", status_code=204) -def delete_transport_mode( +async def delete_transport_mode( key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + user=Depends(get_current_user) ): obj = db.query(TransportMode).filter(TransportMode.key == key).first() if not obj: diff --git a/backend/api/v1/modules/public/reference_data/transport_modes/test_transport_modes.py b/backend/api/v1/modules/public/reference_data/transport_modes/test_transport_modes.py new file mode 100644 index 00000000..34bdb2ca --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/transport_modes/test_transport_modes.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.transport_modes.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_transport_modes(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/transport-modes/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_transport_mode_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/transport-modes/invalid_key", headers=headers) + assert response.status_code == 404 + +def test_create_transport_mode_forbidden(): + response = client.post("/transport-modes/", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_transport_mode_forbidden(): + response = client.put("/transport-modes/TST", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_transport_mode_forbidden(): + response = client.delete("/transport-modes/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/transport_types/dto.py b/backend/api/v1/modules/public/reference_data/transport_types/dto.py index f19c9608..573fc0b3 100644 --- a/backend/api/v1/modules/public/reference_data/transport_types/dto.py +++ b/backend/api/v1/modules/public/reference_data/transport_types/dto.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict class TransportTypeDTO(BaseModel): transport_code: str = Field(..., min_length=1, max_length=2) description: str - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/transport_types/routes.py b/backend/api/v1/modules/public/reference_data/transport_types/routes.py index 7716fbf8..fbcfd42f 100644 --- a/backend/api/v1/modules/public/reference_data/transport_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/transport_types/routes.py @@ -1,15 +1,32 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import has_role, get_current_user from .models import TransportType from .dto import TransportTypeDTO +from typing import Any, Dict + router = APIRouter(prefix="/transport-types", tags=["Transport Types"]) -@router.get("/", response_model=list[TransportTypeDTO]) -def list_transport_types(db: Session = Depends(get_core_db)): - return db.query(TransportType).all() + +@router.get("/", response_model=Dict[str, Any]) +def list_transport_types( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db) +): + skip = (page - 1) * page_size + query = db.query(TransportType) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [TransportTypeDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } @router.get("/{transport_code}", response_model=TransportTypeDTO) def get_transport_type(transport_code: str, db: Session = Depends(get_core_db)): @@ -22,7 +39,7 @@ def get_transport_type(transport_code: str, db: Session = Depends(get_core_db)): def create_transport_type( data: TransportTypeDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + user=Depends(get_current_user) ): obj = TransportType(**data.dict()) db.add(obj) @@ -35,7 +52,7 @@ def update_transport_type( transport_code: str, data: TransportTypeDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + user=Depends(get_current_user) ): obj = db.query(TransportType).filter(TransportType.transport_code == transport_code).first() if not obj: @@ -50,7 +67,7 @@ def update_transport_type( def delete_transport_type( transport_code: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + user=Depends(get_current_user) ): obj = db.query(TransportType).filter(TransportType.transport_code == transport_code).first() if not obj: diff --git a/backend/api/v1/modules/public/reference_data/transport_types/test_transport_types.py b/backend/api/v1/modules/public/reference_data/transport_types/test_transport_types.py new file mode 100644 index 00000000..11256a89 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/transport_types/test_transport_types.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.transport_types.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_transport_types(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/transport-types/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_transport_type_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/transport-types/invalid_code", headers=headers) + assert response.status_code == 404 + +def test_create_transport_type_forbidden(): + response = client.post("/transport-types/", json={"transport_code": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_transport_type_forbidden(): + response = client.put("/transport-types/TST", json={"transport_code": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_transport_type_forbidden(): + response = client.delete("/transport-types/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/valuation_methods/dto.py b/backend/api/v1/modules/public/reference_data/valuation_methods/dto.py index 0ecbca53..a69400a0 100644 --- a/backend/api/v1/modules/public/reference_data/valuation_methods/dto.py +++ b/backend/api/v1/modules/public/reference_data/valuation_methods/dto.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, Field +from pydantic import ConfigDict class ValuationMethodDTO(BaseModel): key: str = Field(..., min_length=1, max_length=2) description: str - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py b/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py index 0fff8021..b4971d32 100644 --- a/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py +++ b/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py @@ -1,25 +1,46 @@ -from fastapi import APIRouter, Depends, HTTPException + +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import has_role +from core.security import get_current_user, has_role from .models import ValuationMethod from .dto import ValuationMethodDTO +from typing import Any, Dict + router = APIRouter(prefix="/valuation-methods", tags=["Valuation Methods"]) -@router.get("/", response_model=list[ValuationMethodDTO]) -def list_valuation_methods(db: Session = Depends(get_core_db)): - return db.query(ValuationMethod).all() + +@router.get("/", response_model=Dict[str, Any]) +async def list_valuation_methods( + page: int = Query(1, ge=1, description="Número de página"), + page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + skip = (page - 1) * page_size + query = db.query(ValuationMethod) + items = query.offset(skip).limit(page_size).all() + total = query.count() + return { + "items": [ValuationMethodDTO.model_validate(obj) for obj in items], + "total": total, + "page": page, + "page_size": page_size + } + + @router.get("/{key}", response_model=ValuationMethodDTO) -def get_valuation_method(key: str, db: Session = Depends(get_core_db)): +async def get_valuation_method(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): obj = db.query(ValuationMethod).filter(ValuationMethod.key == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj + @router.post("/", response_model=ValuationMethodDTO, status_code=201) -def create_valuation_method( +async def create_valuation_method( data: ValuationMethodDTO, db: Session = Depends(get_core_db), current_user: dict = Depends(has_role("admin")) @@ -30,8 +51,9 @@ def create_valuation_method( db.refresh(obj) return obj + @router.put("/{key}", response_model=ValuationMethodDTO) -def update_valuation_method( +async def update_valuation_method( key: str, data: ValuationMethodDTO, db: Session = Depends(get_core_db), @@ -46,8 +68,9 @@ def update_valuation_method( db.refresh(obj) return obj + @router.delete("/{key}", status_code=204) -def delete_valuation_method( +async def delete_valuation_method( key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(has_role("admin")) diff --git a/backend/api/v1/modules/public/reference_data/valuation_methods/test_valuation_methods.py b/backend/api/v1/modules/public/reference_data/valuation_methods/test_valuation_methods.py new file mode 100644 index 00000000..e239d9e6 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/valuation_methods/test_valuation_methods.py @@ -0,0 +1,35 @@ +import pytest +from fastapi.testclient import TestClient +from api.v1.modules.public.reference_data.valuation_methods.routes import router +from fastapi import FastAPI + +app = FastAPI() +app.include_router(router) +client = TestClient(app) + +@pytest.mark.usefixtures("client", "access_token") +def test_list_valuation_methods(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/valuation-methods/", headers=headers) + assert response.status_code == 200 + assert "items" in response.json() + assert "page" in response.json() + assert "page_size" in response.json() + +@pytest.mark.usefixtures("client", "access_token") +def test_get_valuation_method_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/valuation-methods/invalid_key", headers=headers) + assert response.status_code == 404 + +def test_create_valuation_method_forbidden(): + response = client.post("/valuation-methods/", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_update_valuation_method_forbidden(): + response = client.put("/valuation-methods/TST", json={"key": "TST", "description": "Test"}) + assert response.status_code in (403, 405, 404) + +def test_delete_valuation_method_forbidden(): + response = client.delete("/valuation-methods/TST") + assert response.status_code in (403, 405, 404) diff --git a/backend/core/database.py b/backend/core/database.py index 1caa6c64..6c107270 100644 --- a/backend/core/database.py +++ b/backend/core/database.py @@ -4,7 +4,7 @@ Configuración de base de datos con soporte multi-tenant - Bases de datos dedicadas para clientes enterprise """ from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import declarative_base from sqlalchemy.orm import sessionmaker, Session from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from typing import Generator, Dict, Optional, AsyncGenerator diff --git a/backend/core/middleware.py b/backend/core/middleware.py index 459c1fc7..1fbc2c23 100644 --- a/backend/core/middleware.py +++ b/backend/core/middleware.py @@ -25,26 +25,24 @@ class TenantMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next: Callable): # Rutas públicas que no requieren tenant - public_paths = [ - "/api/docs", + # Permitir acceso sin autenticación a rutas de documentación y salud + doc_prefixes = [ "/api/redoc", - "/api/openapi.json", - "/api/v1/auth", - "/api/v1/auth", - "/api/v1/status", + "/api/openapi.json" + ] + public_prefixes = [ + "/api/v1/auth", "/api/v1/status", "/api/health", "/api/" ] - - # Verificar si la ruta es pública (comparación exacta o prefijo) - is_public = False - for path in public_paths: - if request.url.path == path or (path != "/" and request.url.path.startswith(path)): - is_public = True - break - - if is_public: + + path = request.url.path + # Permitir cualquier subruta de docs/redoc/openapi.json (por ejemplo, /api/docs, /api/docs/, /api/docs/oauth2-redirect) + if any(path == prefix or path.startswith(prefix + "/") for prefix in doc_prefixes): + return await call_next(request) + # Permitir rutas públicas exactas o con prefijo + if any(path == prefix or (prefix != "/" and path.startswith(prefix)) for prefix in public_prefixes): return await call_next(request) # Extraer token y obtener tenant