Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/modelseed_api/schemas/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from typing import Literal, Optional

from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, Field, field_validator, model_validator


# Valid template_type values. Mirrors TEMPLATE_FILES in jobs/tasks.py
Expand Down Expand Up @@ -87,6 +87,29 @@ class ReconstructionRequest(BaseModel):
media: Optional[str] = None
output_path: Optional[str] = None

@field_validator("genome_fasta")
@classmethod
def _validate_genome_fasta_content(cls, v):
if v is None:
return v
has_seq = False
in_record = False
for line in v.splitlines():
stripped = line.strip()
if stripped.startswith(">"):
in_record = True
elif stripped and in_record:
has_seq = True
break
if not has_seq:
raise ValueError(
"genome_fasta must contain at least one FASTA record "
"(a >header line followed by sequence data); got no valid "
"records. Pass actual FASTA content, not a file path or "
"workspace reference."
)
return v

@model_validator(mode="after")
def _validate_input_modes(self) -> "ReconstructionRequest":
"""At most ONE of (genome_fasta, rast_job_id) may be set.
Expand Down
30 changes: 30 additions & 0 deletions tests/routes/test_job_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,36 @@ def test_with_dna_fasta(self, local_client, auth_headers):
assert resp.status_code == 200


def test_invalid_genome_fasta_no_sequence_returns_422(self, local_client, auth_headers):
# Headers-only FASTA (no sequence data) must be rejected at request
# time with a 422, not bubble up as a Celery task failure.
resp = local_client.post(
"/api/jobs/reconstruct",
json={"genome": "custom", "genome_fasta": ">contig1\n>contig2\n"},
headers=auth_headers,
)
assert resp.status_code == 422
assert "FASTA" in resp.text

def test_genome_fasta_as_path_returns_422(self, local_client, auth_headers):
# A workspace path passed as genome_fasta content should be rejected.
resp = local_client.post(
"/api/jobs/reconstruct",
json={"genome": "custom", "genome_fasta": "/username/data/genome.fasta"},
headers=auth_headers,
)
assert resp.status_code == 422
assert "FASTA" in resp.text

def test_whitespace_only_genome_fasta_returns_422(self, local_client, auth_headers):
resp = local_client.post(
"/api/jobs/reconstruct",
json={"genome": "custom", "genome_fasta": " \n \n"},
headers=auth_headers,
)
assert resp.status_code == 422


class TestGapfillJob:
def test_dispatch(self, local_client, auth_headers):
resp = local_client.post(
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/test_reconstruction_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Unit tests for ReconstructionRequest schema validation."""
import pytest
from pydantic import ValidationError
from modelseed_api.schemas.jobs import ReconstructionRequest


class TestGenomeFastaValidation:
def test_none_is_valid(self):
req = ReconstructionRequest(genome="83333.1", genome_fasta=None)
assert req.genome_fasta is None

def test_valid_protein_fasta(self):
req = ReconstructionRequest(genome="custom", genome_fasta=">p1\nMKKLVAV")
assert req.genome_fasta is not None

def test_valid_dna_fasta(self):
req = ReconstructionRequest(genome="custom", genome_fasta=">contig1\nACGTACGT")
assert req.genome_fasta is not None

def test_headers_only_rejected(self):
with pytest.raises(ValidationError, match="FASTA"):
ReconstructionRequest(genome="custom", genome_fasta=">c1\n>c2\n")

def test_path_string_rejected(self):
with pytest.raises(ValidationError, match="FASTA"):
ReconstructionRequest(genome="custom", genome_fasta="/user/data/genome.fasta")

def test_whitespace_only_rejected(self):
with pytest.raises(ValidationError, match="FASTA"):
ReconstructionRequest(genome="custom", genome_fasta=" \n ")