diff --git a/backend/api/v1/modules/a76/classes/routes.py b/backend/api/v1/modules/a76/classes/routes.py index e602a0e5..ffea97f0 100644 --- a/backend/api/v1/modules/a76/classes/routes.py +++ b/backend/api/v1/modules/a76/classes/routes.py @@ -35,6 +35,13 @@ async def list_classes( """ List classes with optional filters and pagination """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = ClassService(db) search_params = ClassSearchDTO( client_id=client_id, @@ -58,6 +65,13 @@ async def get_classes_by_client( """ Get all classes for a specific client """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = ClassService(db) return service.search_by_client(client_id, skip, limit) diff --git a/backend/api/v1/modules/a76/classes/test_classes.py b/backend/api/v1/modules/a76/classes/test_classes.py new file mode 100644 index 00000000..0b907703 --- /dev/null +++ b/backend/api/v1/modules/a76/classes/test_classes.py @@ -0,0 +1,31 @@ +import pytest +from fastapi.testclient import TestClient +from .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_classes(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/classes/", 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_class_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/classes/invalid_id", headers=headers) + assert response.status_code == 404 + +def test_create_class_forbidden(): + response = client.post("/classes/", json={"name": "Test Class"}) + assert response.status_code in (403, 405, 404) + +def test_update_class_forbidden(): + response = client.put("/classes/1", json={"name": "Updated Class"}) + assert response.status_code in (403, 405, 404) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/client_and_provider/routes.py b/backend/api/v1/modules/a76/client_and_provider/routes.py index 02d215b8..cafb6428 100644 --- a/backend/api/v1/modules/a76/client_and_provider/routes.py +++ b/backend/api/v1/modules/a76/client_and_provider/routes.py @@ -28,6 +28,17 @@ async def create_client_provider( """ Create a new client or provider in the system """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + + # Ensure the client_data is associated with the correct tenant and company + if client_data.tenant_id != tenant_id or client_data.company_id != company_id: + raise HTTPException(status_code=400, detail="Mismatch in tenant or company association") + service = ClientProviderService(db) return service.create_client_provider(client_data) @@ -45,6 +56,13 @@ async def list_clients_providers( """ List clients and providers with optional filters and pagination """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = ClientProviderService(db) return service.list_clients_providers(skip, limit, search, client_or_provider, enabled_only) @@ -59,6 +77,13 @@ async def get_clients_only( """ Get only clients (client_or_provider = 'C') """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = ClientProviderService(db) return service.get_clients_only(skip, limit) @@ -73,6 +98,13 @@ async def get_providers_only( """ Get only providers (client_or_provider = 'P') """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = ClientProviderService(db) return service.get_providers_only(skip, limit) @@ -86,6 +118,13 @@ async def search_by_rfc( """ Search clients/providers by RFC """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = ClientProviderService(db) return service.search_by_rfc(rfc) @@ -99,6 +138,13 @@ async def get_client_provider( """ Get client/provider by ID with all related information """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = ClientProviderService(db) client = service.get_client_provider(client_id) if not client: @@ -116,6 +162,13 @@ async def update_client_provider( """ Update client/provider information """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = ClientProviderService(db) client = service.update_client_provider(client_id, client_data) if not client: @@ -134,6 +187,13 @@ async def delete_client_provider( Note: This will completely remove the client/provider and all related data. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = ClientProviderService(db) if not service.delete_client_provider(client_id): raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found") @@ -148,6 +208,13 @@ async def toggle_client_provider_status( """ Toggle client/provider enabled/disabled status """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = ClientProviderService(db) client = service.toggle_status(client_id) if not client: @@ -165,6 +232,13 @@ async def get_client_provider_address( """ Get only address information for a client/provider """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = ClientProviderService(db) client = service.get_client_provider(client_id) if not client: @@ -185,6 +259,13 @@ async def get_client_provider_programs( """ Get only programs information for a client/provider """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = ClientProviderService(db) client = service.get_client_provider(client_id) if not client: @@ -205,6 +286,13 @@ async def get_client_provider_basic_info( """ Get basic information for a client/provider (without address and programs) """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = ClientProviderService(db) client = service.get_client_provider(client_id) if not client: diff --git a/backend/api/v1/modules/a76/client_and_provider/test_client_and_provider.py b/backend/api/v1/modules/a76/client_and_provider/test_client_and_provider.py new file mode 100644 index 00000000..55f319b6 --- /dev/null +++ b/backend/api/v1/modules/a76/client_and_provider/test_client_and_provider.py @@ -0,0 +1,31 @@ +import pytest +from fastapi.testclient import TestClient +from .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_clients_and_providers(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/client_and_provider/", 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_client_or_provider_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/client_and_provider/invalid_id", headers=headers) + assert response.status_code == 404 + +def test_create_client_or_provider_forbidden(): + response = client.post("/client_and_provider/", json={"name": "Test Client/Provider"}) + assert response.status_code in (403, 405, 404) + +def test_update_client_or_provider_forbidden(): + response = client.put("/client_and_provider/1", json={"name": "Updated Client/Provider"}) + assert response.status_code in (403, 405, 404) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/company/test_company.py b/backend/api/v1/modules/a76/company/test_company.py new file mode 100644 index 00000000..206c8c38 --- /dev/null +++ b/backend/api/v1/modules/a76/company/test_company.py @@ -0,0 +1,31 @@ +import pytest +from fastapi.testclient import TestClient +from .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_companies(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/company/", 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_company_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/company/invalid_id", headers=headers) + assert response.status_code == 404 + +def test_create_company_forbidden(): + response = client.post("/company/", json={"name": "Test Company"}) + assert response.status_code in (403, 405, 404) + +def test_update_company_forbidden(): + response = client.put("/company/1", json={"name": "Updated Company"}) + assert response.status_code in (403, 405, 404) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/country_rule_oct/routes.py b/backend/api/v1/modules/a76/country_rule_oct/routes.py index ca15fa92..3d40c0b9 100644 --- a/backend/api/v1/modules/a76/country_rule_oct/routes.py +++ b/backend/api/v1/modules/a76/country_rule_oct/routes.py @@ -18,6 +18,13 @@ async def list_countries( """ List all CountryRuleOct entries. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + return db.query(CountryRuleOctService).all() @@ -33,6 +40,13 @@ async def read_country_rule( """ Get a specific CountryRuleOct by its composite key. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + country = CountryRuleOctService.get_country_by_keys(db, permission, line, fraction, country_code) if not country: raise HTTPException(status_code=404, detail="CountryRuleOct not found") diff --git a/backend/api/v1/modules/a76/country_rule_oct/test_country_rule_oct.py b/backend/api/v1/modules/a76/country_rule_oct/test_country_rule_oct.py new file mode 100644 index 00000000..b4354146 --- /dev/null +++ b/backend/api/v1/modules/a76/country_rule_oct/test_country_rule_oct.py @@ -0,0 +1,31 @@ +import pytest +from fastapi.testclient import TestClient +from .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_country_rules(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/country-rule-oct/", 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_rule_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/country-rule-oct/invalid_id", headers=headers) + assert response.status_code == 404 + +def test_create_country_rule_forbidden(): + response = client.post("/country-rule-oct/", json={"rule": "Test Rule"}) + assert response.status_code in (403, 405, 404) + +def test_update_country_rule_forbidden(): + response = client.put("/country-rule-oct/1", json={"rule": "Updated Rule"}) + assert response.status_code in (403, 405, 404) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/exchange_rate/routes.py b/backend/api/v1/modules/a76/exchange_rate/routes.py index 48b6e330..0446f3c0 100644 --- a/backend/api/v1/modules/a76/exchange_rate/routes.py +++ b/backend/api/v1/modules/a76/exchange_rate/routes.py @@ -18,6 +18,13 @@ async def list_exchange_rates( """ List all ExchangeRate entries. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + return db.query(ExchangeRateService).all() @@ -30,6 +37,13 @@ async def read_exchange_rate( """ Get a specific ExchangeRate by its date. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + exchange_rate = ExchangeRateService.get_exchange_rate_by_date(db, date) if not exchange_rate: raise HTTPException(status_code=404, detail="ExchangeRate not found") @@ -45,6 +59,13 @@ async def create_exchange_rate( """ Create a new ExchangeRate entry. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + return ExchangeRateService.create_exchange_rate(db, exchange_rate_data) @@ -57,6 +78,13 @@ async def delete_exchange_rate( """ Delete an ExchangeRate by its date. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + exchange_rate = ExchangeRateService.delete_exchange_rate(db, date) if not exchange_rate: raise HTTPException(status_code=404, detail="ExchangeRate not found") \ No newline at end of file diff --git a/backend/api/v1/modules/a76/exchange_rate/test_exchange_rate.py b/backend/api/v1/modules/a76/exchange_rate/test_exchange_rate.py new file mode 100644 index 00000000..caf3d0b9 --- /dev/null +++ b/backend/api/v1/modules/a76/exchange_rate/test_exchange_rate.py @@ -0,0 +1,31 @@ +import pytest +from fastapi.testclient import TestClient +from .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_exchange_rates(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/exchange-rate/", 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_exchange_rate_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/exchange-rate/invalid_id", headers=headers) + assert response.status_code == 404 + +def test_create_exchange_rate_forbidden(): + response = client.post("/exchange-rate/", json={"rate": 1.23}) + assert response.status_code in (403, 405, 404) + +def test_update_exchange_rate_forbidden(): + response = client.put("/exchange-rate/1", json={"rate": 1.45}) + assert response.status_code in (403, 405, 404) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/fraction_rule_octave/routes.py b/backend/api/v1/modules/a76/fraction_rule_octave/routes.py index 43c24bc5..ab5b4363 100644 --- a/backend/api/v1/modules/a76/fraction_rule_octave/routes.py +++ b/backend/api/v1/modules/a76/fraction_rule_octave/routes.py @@ -18,6 +18,13 @@ async def list_fractions( """ List all FractionRuleOctave entries. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + return db.query(FractionRuleOctaveService).all() @@ -32,6 +39,13 @@ async def read_fraction( """ Get a specific FractionRuleOctave by its composite key. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + frac = FractionRuleOctaveService.get_fraction_by_permission_line(db, permission, line, fraction) if not frac: raise HTTPException(status_code=404, detail="FractionRuleOctave not found") @@ -47,6 +61,13 @@ async def create_frac( """ Create a new FractionRuleOctave entry. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + return FractionRuleOctaveService.create_frac(db, frac_data) @@ -61,6 +82,13 @@ async def delete_fraction( """ Delete a FractionRuleOctave by its composite key. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + frac = FractionRuleOctaveService.delete_fraction(db, permission, line, fraction) if not frac: raise HTTPException(status_code=404, detail="FractionRuleOctave not found") \ No newline at end of file diff --git a/backend/api/v1/modules/a76/fraction_rule_octave/test_fraction_rule_octave.py b/backend/api/v1/modules/a76/fraction_rule_octave/test_fraction_rule_octave.py new file mode 100644 index 00000000..c4d67c5b --- /dev/null +++ b/backend/api/v1/modules/a76/fraction_rule_octave/test_fraction_rule_octave.py @@ -0,0 +1,31 @@ +import pytest +from fastapi.testclient import TestClient +from .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_fraction_rules(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/fraction_rule_octave/", 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_fraction_rule_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/fraction_rule_octave/invalid_id", headers=headers) + assert response.status_code == 404 + +def test_create_fraction_rule_forbidden(): + response = client.post("/fraction_rule_octave/", json={"rule": "Test Rule"}) + assert response.status_code in (403, 405, 404) + +def test_update_fraction_rule_forbidden(): + response = client.put("/fraction_rule_octave/1", json={"rule": "Updated Rule"}) + assert response.status_code in (403, 405, 404) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/package/routes.py b/backend/api/v1/modules/a76/package/routes.py index c8b17ba7..de08247e 100644 --- a/backend/api/v1/modules/a76/package/routes.py +++ b/backend/api/v1/modules/a76/package/routes.py @@ -21,6 +21,13 @@ async def list_bultos( """ List all GBultos with pagination. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + return db.query(Package).offset(skip).limit(limit).all() @@ -33,6 +40,13 @@ async def read_bulto( """ Get a specific Package by its CODE. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + bulto = GBultoService.get_bulto_by_code(db, code) if not bulto: raise HTTPException(status_code=404, detail="Package not found") @@ -48,6 +62,13 @@ async def create_gbulto( """ Create a new Package. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + return GBultoService.create_gbulto(db, bulto_data) @@ -61,6 +82,13 @@ async def update_bulto( """ Update an existing Package. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + bulto = GBultoService.update_bulto(db, code, bulto_data) if not bulto: raise HTTPException(status_code=404, detail="Package not found") @@ -76,6 +104,13 @@ async def delete_bulto( """ Delete a Package by its CODE. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + bulto = GBultoService.delete_bulto(db, code) if not bulto: raise HTTPException(status_code=404, detail="Package not found") \ No newline at end of file diff --git a/backend/api/v1/modules/a76/package/test_package.py b/backend/api/v1/modules/a76/package/test_package.py new file mode 100644 index 00000000..4d0d5bd7 --- /dev/null +++ b/backend/api/v1/modules/a76/package/test_package.py @@ -0,0 +1,31 @@ +import pytest +from fastapi.testclient import TestClient +from .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_packages(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/bultos/", 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_package_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/bultos/invalid_id", headers=headers) + assert response.status_code == 404 + +def test_create_package_forbidden(): + response = client.post("/bultos/", json={"name": "Test Package"}) + assert response.status_code in (403, 405, 404) + +def test_update_package_forbidden(): + response = client.put("/bultos/1", json={"name": "Updated Package"}) + assert response.status_code in (403, 405, 404) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/parts/routes.py b/backend/api/v1/modules/a76/parts/routes.py index 3c4e2691..7aab6c88 100644 --- a/backend/api/v1/modules/a76/parts/routes.py +++ b/backend/api/v1/modules/a76/parts/routes.py @@ -29,6 +29,13 @@ async def create_part( """ Create a new part in the system """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = PartService(db) return service.create_part(part_data) @@ -49,6 +56,13 @@ async def list_parts( """ List parts with optional filters and pagination """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = PartService(db) search_params = PartSearchDTO( client_id=client_id, @@ -72,6 +86,13 @@ async def get_parts_by_client( """ Get all parts for a specific client """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = PartService(db) return service.search_by_client(client_id, skip, limit) @@ -85,6 +106,13 @@ async def search_by_fraction( """ Search parts by tariff fraction """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = PartService(db) return service.search_by_fraction(fraction) @@ -98,6 +126,13 @@ async def search_by_supplier( """ Search parts by supplier """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = PartService(db) return service.search_by_supplier(supplier) @@ -111,6 +146,13 @@ async def get_parts_by_country( """ Get parts by country of origin """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = PartService(db) return service.get_parts_by_country(country_code) @@ -123,6 +165,13 @@ async def get_parts_statistics( """ Get basic parts statistics """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = PartService(db) return service.get_parts_statistics() @@ -137,6 +186,13 @@ async def get_part( """ Get part by composite key (client_id + part_number) """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = PartService(db) part = service.get_part(client_id, part_number) if not part: @@ -158,6 +214,13 @@ async def update_part( """ Update part information """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = PartService(db) part = service.update_part(client_id, part_number, part_data) if not part: @@ -180,6 +243,13 @@ async def delete_part( Note: This will completely remove the part from the system. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = PartService(db) if not service.delete_part(client_id, part_number): raise HTTPException( @@ -198,6 +268,13 @@ async def toggle_part_status( """ Toggle part enabled/disabled status """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = PartService(db) part = service.toggle_status(client_id, part_number) if not part: @@ -219,6 +296,13 @@ async def get_part_basic_info( """ Get basic information for a part """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = PartService(db) part = service.get_part(client_id, part_number) if not part: @@ -249,6 +333,13 @@ async def get_part_regulatory_info( """ Get regulatory information for a part (FDA, FCC, ECCN, etc.) """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + service = PartService(db) part = service.get_part(client_id, part_number) if not part: diff --git a/backend/api/v1/modules/a76/parts/test_parts.py b/backend/api/v1/modules/a76/parts/test_parts.py new file mode 100644 index 00000000..1596c8b0 --- /dev/null +++ b/backend/api/v1/modules/a76/parts/test_parts.py @@ -0,0 +1,31 @@ +import pytest +from fastapi.testclient import TestClient +from .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_parts(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/parts/", 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_part_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/parts/invalid_id", headers=headers) + assert response.status_code == 404 + +def test_create_part_forbidden(): + response = client.post("/parts/", json={"name": "Test Part"}) + assert response.status_code in (403, 405, 404) + +def test_update_part_forbidden(): + response = client.put("/parts/1", json={"name": "Updated Part"}) + assert response.status_code in (403, 405, 404) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/permission_rule_oct/routes.py b/backend/api/v1/modules/a76/permission_rule_oct/routes.py index 75fcae79..34a20e5e 100644 --- a/backend/api/v1/modules/a76/permission_rule_oct/routes.py +++ b/backend/api/v1/modules/a76/permission_rule_oct/routes.py @@ -18,6 +18,13 @@ async def list_permissions( """ List all PermissionRuleOct entries. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + return db.query(PermissionRuleOctService).all() @@ -30,6 +37,13 @@ async def read_permission( """ Get a specific PermissionRuleOct by its permission. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + permission = PermissionRuleOctService.get_permission_by_id(db, permission) if not permission: raise HTTPException(status_code=404, detail="PermissionRuleOct not found") @@ -45,6 +59,13 @@ async def create_permission( """ Create a new PermissionRuleOct entry. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + return PermissionRuleOctService.create_permission(db, permission_data) @@ -57,6 +78,13 @@ async def delete_permission( """ Delete a PermissionRuleOct by its permission. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + permission = PermissionRuleOctService.delete_permission(db, permission) if not permission: raise HTTPException(status_code=404, detail="PermissionRuleOct not found") \ No newline at end of file diff --git a/backend/api/v1/modules/a76/permission_rule_oct/test_permission_rule_oct.py b/backend/api/v1/modules/a76/permission_rule_oct/test_permission_rule_oct.py new file mode 100644 index 00000000..f4ee706b --- /dev/null +++ b/backend/api/v1/modules/a76/permission_rule_oct/test_permission_rule_oct.py @@ -0,0 +1,31 @@ +import pytest +from fastapi.testclient import TestClient +from .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_permission_rules(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/permission_rule_oct/", 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_permission_rule_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/permission_rule_oct/invalid_id", headers=headers) + assert response.status_code == 404 + +def test_create_permission_rule_forbidden(): + response = client.post("/permission_rule_oct/", json={"rule": "Test Rule"}) + assert response.status_code in (403, 405, 404) + +def test_update_permission_rule_forbidden(): + response = client.put("/permission_rule_oct/1", json={"rule": "Updated Rule"}) + assert response.status_code in (403, 405, 404) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/seal/routes.py b/backend/api/v1/modules/a76/seal/routes.py index cbd60d92..ba1e6892 100644 --- a/backend/api/v1/modules/a76/seal/routes.py +++ b/backend/api/v1/modules/a76/seal/routes.py @@ -22,6 +22,13 @@ async def list_seals( """ List all Seal entries. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + return db.query(SealService).all() @@ -34,6 +41,13 @@ async def read_seal( """ Get a specific Seal by its seal. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + seal = SealService.get_seal_by_id(db, seal) if not seal: raise HTTPException(status_code=404, detail="Seal not found") @@ -49,6 +63,13 @@ async def create_seal( """ Create a new Seal entry. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + return SealService.create_seal(db, seal_data) @@ -61,6 +82,13 @@ async def delete_seal( """ Delete a Seal by its seal. """ + # Validate access to the tenant and company + tenant_id = current_user.get("tenant_id") + company_id = current_user.get("company_id") + + if not tenant_id or not company_id: + raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + seal = SealService.delete_seal(db, seal) if not seal: raise HTTPException(status_code=404, detail="Seal not found") \ No newline at end of file diff --git a/backend/api/v1/modules/a76/seal/test_seal.py b/backend/api/v1/modules/a76/seal/test_seal.py new file mode 100644 index 00000000..80e35b91 --- /dev/null +++ b/backend/api/v1/modules/a76/seal/test_seal.py @@ -0,0 +1,31 @@ +import pytest +from fastapi.testclient import TestClient +from .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_seals(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/seal/", 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_seal_not_found(client, access_token): + headers = {"Authorization": f"Bearer {access_token}"} + response = client.get("/seal/invalid_id", headers=headers) + assert response.status_code == 404 + +def test_create_seal_forbidden(): + response = client.post("/seal/", json={"name": "Test Seal"}) + assert response.status_code in (403, 405, 404) + +def test_update_seal_forbidden(): + response = client.put("/seal/1", json={"name": "Updated Seal"}) + assert response.status_code in (403, 405, 404) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/user_tenant/models.py b/backend/api/v1/modules/a76/user_tenant/models.py index e6e141d9..d67ce1ee 100644 --- a/backend/api/v1/modules/a76/user_tenant/models.py +++ b/backend/api/v1/modules/a76/user_tenant/models.py @@ -1,7 +1,7 @@ """ Modelo de relación entre usuarios (Keycloak) y tenants """ -from sqlalchemy import Integer, String, DateTime, Boolean, UniqueConstraint, ForeignKeyConstraint +from sqlalchemy import Integer, String, DateTime, Boolean, UniqueConstraint, ForeignKeyConstraint, ForeignKey from sqlalchemy.sql import func from sqlalchemy.orm import Mapped, mapped_column, relationship from datetime import datetime @@ -34,6 +34,9 @@ class UserTenant(Base): # ID del tenant tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + # ID de la empresa asociada + company_id: Mapped[int] = mapped_column(Integer, ForeignKey("a76.companies.id"), nullable=False, index=True) + # Estado de la relación is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) diff --git a/backend/core/security.py b/backend/core/security.py index f2842af7..96f29405 100644 --- a/backend/core/security.py +++ b/backend/core/security.py @@ -8,6 +8,9 @@ from jose import jwt, JWTError from typing import Optional, Dict, Any from .config import settings import logging +from sqlalchemy.orm import Session +from core.database import get_core_db +from api.v1.modules.a76.user_tenant.models import UserTenant logger = logging.getLogger(__name__) @@ -75,16 +78,33 @@ def verify_token(token: str) -> Dict[str, Any]: async def get_current_user( - credentials: HTTPAuthorizationCredentials = Security(security) + credentials: HTTPAuthorizationCredentials = Security(security), + db: Session = Depends(get_core_db) ) -> Dict[str, Any]: """ Dependency para obtener el usuario actual desde el token JWT + Enriquecido con tenant_id y company_id desde la tabla user_tenant Uso en FastAPI: current_user: dict = Depends(get_current_user) """ token = credentials.credentials user_info = verify_token(token) + + # Obtener user_id desde el token + user_id = user_info.get("sub") + if not user_id: + raise HTTPException(status_code=401, detail="User ID not found in token") + + # Consultar la tabla user_tenant para obtener tenant_id y company_id + user_tenant = db.query(UserTenant).filter(UserTenant.keycloak_user_id == user_id).first() + if not user_tenant: + raise HTTPException(status_code=403, detail="User does not have access to any tenant or company") + + # Enriquecer user_info con tenant_id y company_id + user_info["tenant_id"] = user_tenant.tenant_id + user_info["company_id"] = user_tenant.company_id + return user_info