Commit 2784adf3 authored by Paris Stentoumis's avatar Paris Stentoumis
Browse files

feat: deleted old file

parent c9474c3d
Loading
Loading
Loading
Loading
Loading
+0 −5
Original line number Diff line number Diff line
[pytest]
testpaths = tests
pythonpath = .
log_cli = true
log_cli_level = WARNING
+0 −137
Original line number Diff line number Diff line
"""
Shared fixtures for SRM controller unit tests.

Sets up:
- A Flask test app (needed for connexion.request context)
- A mock network adapter pre-wired with a standard Location response
- Helper to POST JSON to the retrieve_location controller
"""

import json
import sys
import os
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch

import connexion
import pytest
from flask import Flask

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from sunrise6g_opensdk.network.core.schemas import (
    AreaType,
    Circle,
    Location,
    Point,
)


# ---------------------------------------------------------------------------
# Constants reused across tests
# ---------------------------------------------------------------------------

MOCK_LATITUDE = 48.8566
MOCK_LONGITUDE = 2.3522
MOCK_RADIUS = 150.0
MOCK_TIMESTAMP = datetime(2026, 2, 10, 12, 0, 0, tzinfo=timezone.utc)

PAYLOAD_BY_NAI = {
    "device": {"networkAccessIdentifier": "imsi-001010000000001"},
    "maxAge": 60,
}

PAYLOAD_BY_IP = {
    "device": {
        "ipv4Address": {
            "publicAddress": "12.1.0.1",
            "privateAddress": "12.1.0.1",
        }
    }
}

PAYLOAD_BY_PHONE = {
    "device": {"phoneNumber": "+10010100001"}
}


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture()
def flask_app():
    """Minimal Flask app for request context."""
    return Flask(__name__)


@pytest.fixture()
def mock_location():
    """A CAMARA Location object with arbitrary coordinates for testing."""
    return Location(
        lastLocationTime=MOCK_TIMESTAMP,
        area=Circle(
            areaType=AreaType.circle,
            center=Point(latitude=MOCK_LATITUDE, longitude=MOCK_LONGITUDE),
            radius=MOCK_RADIUS,
        ),
    )


@pytest.fixture()
def mock_adapter(mock_location):
    """A mock network adapter that returns a fixed Location by default."""
    adapter = MagicMock()
    adapter.create_monitoring_event_subscription.return_value = mock_location
    return adapter


@pytest.fixture(autouse=True)
def reset_network_adapter():
    """
    Reset the module-level network_adapter to None before each test so tests
    are fully isolated from each other and from any real env vars.
    """
    import src.controllers.network_functions_controller as ctrl
    original = ctrl.network_adapter
    ctrl.network_adapter = None
    yield
    ctrl.network_adapter = original


def call_retrieve_location(flask_app, payload):
    """
    Helper: call retrieve_location() inside a proper Flask+connexion request
    context with the given JSON payload.
    Returns the controller's return value. For success (body, 200) returns just
    the body for convenience; for errors returns the full (body, status) tuple.
    """
    import src.controllers.network_functions_controller as ctrl

    with flask_app.test_request_context(
        "/location/retrieve",
        method="POST",
        content_type="application/json",
        data=json.dumps(payload),
    ):
        from flask import request as flask_request
        with patch.object(connexion, "request", flask_request):
            ret = ctrl.retrieve_location()
            if isinstance(ret, tuple) and len(ret) == 2 and isinstance(ret[1], int) and 200 <= ret[1] < 300:
                return ret[0]
            return ret


def call_retrieve_location_plain_text(flask_app):
    """Helper: call retrieve_location() with non-JSON content."""
    import src.controllers.network_functions_controller as ctrl

    with flask_app.test_request_context(
        "/location/retrieve",
        method="POST",
        content_type="text/plain",
        data="not json",
    ):
        from flask import request as flask_request
        with patch.object(connexion, "request", flask_request):
            return ctrl.retrieve_location()
+0 −214
Original line number Diff line number Diff line
"""
Unit tests for the retrieve_location controller.

Structure:
  TestStubMode       – no adapter configured (NETWORK_ADAPTER_NAME not set)
  TestWithAdapter    – adapter is configured, SDK returns a Location object
  TestInputValidation – bad / edge-case request bodies
  TestErrorHandling  – adapter raises exceptions
"""

import pytest
from conftest import (
    MOCK_LATITUDE,
    MOCK_LONGITUDE,
    MOCK_RADIUS,
    PAYLOAD_BY_IP,
    PAYLOAD_BY_NAI,
    PAYLOAD_BY_PHONE,
    call_retrieve_location,
    call_retrieve_location_plain_text,
)


# ---------------------------------------------------------------------------
# Stub mode (no adapter)
# ---------------------------------------------------------------------------

class TestStubMode:
    """When NETWORK_ADAPTER_NAME is not set the controller returns 503 UNAVAILABLE."""

    def test_returns_503_when_no_adapter(self, flask_app):
        body, status = call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        assert status == 503

    def test_503_body_has_code_unavailable(self, flask_app):
        body, status = call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        assert body.get("code") == "UNAVAILABLE"

    def test_503_body_has_message(self, flask_app):
        body, status = call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        assert "message" in body
        assert "Network Adapter" in body["message"]

    def test_non_json_returns_400(self, flask_app):
        result = call_retrieve_location_plain_text(flask_app)
        assert result == ({"error": "Could not read JSON payload."}, 400)


# ---------------------------------------------------------------------------
# With adapter (mocked SDK)
# ---------------------------------------------------------------------------

class TestWithAdapter:
    """When an adapter is set the controller must delegate to it correctly."""

    @pytest.fixture(autouse=True)
    def inject_adapter(self, mock_adapter):
        import src.controllers.network_functions_controller as ctrl
        ctrl.network_adapter = mock_adapter

    # --- Response structure ---

    def test_returns_dict(self, flask_app):
        result = call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        assert isinstance(result, dict)

    def test_response_has_last_location_time(self, flask_app):
        result = call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        assert "lastLocationTime" in result

    def test_response_last_location_time_is_iso_string(self, flask_app):
        result = call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        # model_dump(mode='json') must have converted datetime → ISO string
        assert isinstance(result["lastLocationTime"], str)
        assert "2026-02-10" in result["lastLocationTime"]

    def test_response_has_area(self, flask_app):
        result = call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        assert "area" in result

    def test_response_area_type_is_circle(self, flask_app):
        result = call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        assert result["area"]["areaType"] == "CIRCLE"

    def test_response_coordinates(self, flask_app):
        area = call_retrieve_location(flask_app, PAYLOAD_BY_NAI)["area"]
        assert area["center"]["latitude"] == MOCK_LATITUDE
        assert area["center"]["longitude"] == MOCK_LONGITUDE
        assert area["radius"] == MOCK_RADIUS

    def test_no_none_values_in_response(self, flask_app):
        """model_dump(exclude_none=True) must strip None fields."""
        result = call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        def has_none(obj):
            if isinstance(obj, dict):
                return None in obj.values() or any(has_none(v) for v in obj.values())
            return False
        assert not has_none(result)

    # --- Adapter is called with the right model ---

    def test_adapter_called_once(self, flask_app, mock_adapter):
        call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        mock_adapter.create_monitoring_event_subscription.assert_called_once()

    def test_adapter_receives_retrieval_location_request(self, flask_app, mock_adapter):
        from sunrise6g_opensdk.network.core.schemas import RetrievalLocationRequest
        call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        arg = mock_adapter.create_monitoring_event_subscription.call_args.args[0]
        assert isinstance(arg, RetrievalLocationRequest)

    def test_adapter_receives_correct_nai(self, flask_app, mock_adapter):
        call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        arg = mock_adapter.create_monitoring_event_subscription.call_args.args[0]
        assert arg.device.networkAccessIdentifier is not None

    def test_adapter_receives_correct_max_age(self, flask_app, mock_adapter):
        call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        arg = mock_adapter.create_monitoring_event_subscription.call_args.args[0]
        assert arg.maxAge == 60

    # --- Different device identifier types ---

    def test_identify_by_ip(self, flask_app, mock_adapter):
        result = call_retrieve_location(flask_app, PAYLOAD_BY_IP)
        assert "area" in result
        arg = mock_adapter.create_monitoring_event_subscription.call_args.args[0]
        assert arg.device.ipv4Address is not None

    def test_identify_by_phone(self, flask_app, mock_adapter):
        result = call_retrieve_location(flask_app, PAYLOAD_BY_PHONE)
        assert "area" in result
        arg = mock_adapter.create_monitoring_event_subscription.call_args.args[0]
        assert arg.device.phoneNumber is not None

    def test_non_json_still_returns_400_with_adapter(self, flask_app):
        result = call_retrieve_location_plain_text(flask_app)
        assert result == ({"error": "Could not read JSON payload."}, 400)


# ---------------------------------------------------------------------------
# Input validation
# ---------------------------------------------------------------------------

class TestInputValidation:
    """Invalid request bodies must be handled gracefully, never crash."""

    @pytest.fixture(autouse=True)
    def inject_adapter(self, mock_adapter):
        import src.controllers.network_functions_controller as ctrl
        ctrl.network_adapter = mock_adapter

    def test_missing_device_field(self, flask_app, mock_adapter):
        """No device key → Pydantic accepts it (device is Optional) but adapter may reject."""
        # Controller must not crash — it should either return a result or a 500
        result = call_retrieve_location(flask_app, {"maxAge": 30})
        assert isinstance(result, dict) or (isinstance(result, tuple) and result[1] == 500)

    def test_empty_body(self, flask_app, mock_adapter):
        result = call_retrieve_location(flask_app, {})
        assert isinstance(result, dict) or (isinstance(result, tuple) and result[1] == 500)

    def test_max_age_is_passed_when_provided(self, flask_app, mock_adapter):
        call_retrieve_location(flask_app, {"device": {"networkAccessIdentifier": "imsi-x"}, "maxAge": 120})
        arg = mock_adapter.create_monitoring_event_subscription.call_args.args[0]
        assert arg.maxAge == 120

    def test_max_age_is_none_when_omitted(self, flask_app, mock_adapter):
        call_retrieve_location(flask_app, {"device": {"networkAccessIdentifier": "imsi-x"}})
        arg = mock_adapter.create_monitoring_event_subscription.call_args.args[0]
        assert arg.maxAge is None

    def test_max_surface_is_passed_when_provided(self, flask_app, mock_adapter):
        payload = {"device": {"networkAccessIdentifier": "imsi-x"}, "maxSurface": 5000}
        call_retrieve_location(flask_app, payload)
        arg = mock_adapter.create_monitoring_event_subscription.call_args.args[0]
        assert arg.maxSurface == 5000


# ---------------------------------------------------------------------------
# Error handling
# ---------------------------------------------------------------------------

class TestErrorHandling:
    """Adapter errors must be caught and returned as HTTP 500, never re-raised."""

    @pytest.fixture(autouse=True)
    def inject_adapter(self, mock_adapter):
        import src.controllers.network_functions_controller as ctrl
        ctrl.network_adapter = mock_adapter

    def test_adapter_generic_exception_returns_500(self, flask_app, mock_adapter):
        mock_adapter.create_monitoring_event_subscription.side_effect = Exception("NEF down")
        body, status = call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        assert status == 500

    def test_adapter_exception_message_in_body(self, flask_app, mock_adapter):
        mock_adapter.create_monitoring_event_subscription.side_effect = Exception("connection refused")
        body, status = call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        assert body.get("error") == "connection refused"

    def test_adapter_value_error_returns_500(self, flask_app, mock_adapter):
        mock_adapter.create_monitoring_event_subscription.side_effect = ValueError("bad response from NEF")
        body, status = call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
        assert status == 500

    def test_controller_does_not_reraise(self, flask_app, mock_adapter):
        """The controller must never let an exception bubble up to the caller."""
        mock_adapter.create_monitoring_event_subscription.side_effect = RuntimeError("unexpected")
        try:
            result = call_retrieve_location(flask_app, PAYLOAD_BY_NAI)
            assert result is not None
        except RuntimeError:
            pytest.fail("Controller re-raised the exception instead of catching it")