138 lines
4.2 KiB
Python
138 lines
4.2 KiB
Python
import json
|
|
import uuid
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from control_backend.api.v1.endpoints import program
|
|
from control_backend.schemas.program import BasicNorm, Goal, Phase, Plan, Program
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
"""Create a FastAPI app with the /program route and mock socket."""
|
|
app = FastAPI()
|
|
app.include_router(program.router)
|
|
return app
|
|
|
|
|
|
@pytest.fixture
|
|
def client(app):
|
|
"""Create a TestClient."""
|
|
return TestClient(app)
|
|
|
|
|
|
def make_valid_program_dict():
|
|
"""Helper to create a valid Program JSON structure."""
|
|
# Converting to JSON using Pydantic because it knows how to convert a UUID object
|
|
program_json_str = Program(
|
|
phases=[
|
|
Phase(
|
|
id=uuid.uuid4(),
|
|
name="Basic Phase",
|
|
norms=[
|
|
BasicNorm(
|
|
id=uuid.uuid4(),
|
|
name="Some norm",
|
|
norm="Do normal.",
|
|
),
|
|
],
|
|
goals=[
|
|
Goal(
|
|
id=uuid.uuid4(),
|
|
name="Some goal",
|
|
description="This description can be used to determine whether the goal "
|
|
"has been achieved.",
|
|
plan=Plan(
|
|
id=uuid.uuid4(),
|
|
name="Goal Plan",
|
|
steps=[],
|
|
),
|
|
can_fail=False,
|
|
),
|
|
],
|
|
triggers=[],
|
|
),
|
|
],
|
|
).model_dump_json()
|
|
# Converting back to a dict because that's what's expected
|
|
return json.loads(program_json_str)
|
|
|
|
|
|
def test_receive_program_success(client):
|
|
"""Valid Program JSON should be parsed and sent through the socket."""
|
|
mock_pub_socket = AsyncMock()
|
|
client.app.state.endpoints_pub_socket = mock_pub_socket
|
|
|
|
program_dict = make_valid_program_dict()
|
|
|
|
response = client.post("/program", json=program_dict)
|
|
|
|
assert response.status_code == 202
|
|
assert response.json() == {"status": "Program parsed"}
|
|
|
|
# Verify socket call
|
|
mock_pub_socket.send_multipart.assert_awaited_once()
|
|
args, kwargs = mock_pub_socket.send_multipart.await_args
|
|
|
|
assert args[0][0] == b"program"
|
|
|
|
sent_bytes = args[0][1]
|
|
sent_obj = json.loads(sent_bytes.decode())
|
|
|
|
# Converting to JSON using Pydantic because it knows how to handle UUIDs
|
|
expected_obj = json.loads(Program.model_validate(program_dict).model_dump_json())
|
|
assert sent_obj == expected_obj
|
|
|
|
|
|
def test_receive_program_invalid_json(client):
|
|
"""
|
|
Invalid JSON (malformed) -> FastAPI never calls endpoint.
|
|
It returns a 422 Unprocessable Entity.
|
|
"""
|
|
mock_pub_socket = AsyncMock()
|
|
client.app.state.endpoints_pub_socket = mock_pub_socket
|
|
|
|
# FastAPI only accepts valid JSON bodies, so send raw string
|
|
response = client.post("/program", content="{invalid json}")
|
|
|
|
assert response.status_code == 422
|
|
mock_pub_socket.send_multipart.assert_not_called()
|
|
|
|
|
|
def test_receive_program_invalid_deep_structure(client):
|
|
"""
|
|
Valid JSON but schema invalid -> Pydantic throws validation error -> 422.
|
|
"""
|
|
mock_pub_socket = AsyncMock()
|
|
client.app.state.endpoints_pub_socket = mock_pub_socket
|
|
|
|
# Missing "value" in norms element
|
|
bad_program = {
|
|
"phases": [
|
|
{
|
|
"id": "phase1",
|
|
"name": "deepfail",
|
|
"nextPhaseId": "phase2",
|
|
"phaseData": {
|
|
"norms": [
|
|
{"id": "n1", "name": "norm"} # INVALID: missing "value"
|
|
],
|
|
"goals": [
|
|
{"id": "g1", "name": "goal", "description": "desc", "achieved": False}
|
|
],
|
|
"triggers": [
|
|
{"id": "t1", "label": "trigger", "type": "keyword", "value": ["start"]}
|
|
],
|
|
},
|
|
}
|
|
]
|
|
}
|
|
|
|
response = client.post("/program", json=bad_program)
|
|
|
|
assert response.status_code == 422
|
|
mock_pub_socket.send_multipart.assert_not_called()
|