forked from afanasyev.aa/stratoflights
74 lines
3.3 KiB
Python
74 lines
3.3 KiB
Python
from django.test import SimpleTestCase
|
|
|
|
from .serializers import PredictionRequestSerializer
|
|
|
|
|
|
class LaunchLongitudeTests(SimpleTestCase):
|
|
"""
|
|
This API does not bound longitude. The predictor does.
|
|
|
|
Longitude lives on a circle: every real number names a real meridian, and
|
|
5170 is a legitimate way to write 50 E. So any interval is a policy about
|
|
typos, not about places, and there is exactly one place that policy belongs —
|
|
the service that owns the wind grid. Two layers each guessing at it is how the
|
|
original defect happened: this serializer declared min_value=0, narrower than
|
|
the endpoint it calls, and refused every launch west of Greenwich. Canada,
|
|
Greenland and Alaska, i.e. most of the Arctic sites this product targets.
|
|
|
|
The endpoint actually called is GET /api/v1/prediction, which applies no
|
|
longitude bound at all — it normalises and lets the grid refuse what it cannot
|
|
use. Mirroring that exactly is the point: this layer must never be the narrower
|
|
one.
|
|
|
|
Nuuk is -51.7. The same meridian written unsigned is 308.3, and that always
|
|
worked, which is what showed the constraint was about notation, not place.
|
|
|
|
Non-numbers are still refused here, by FloatField. NaN and infinity are not:
|
|
FloatField parses them, and the predictor's request decoder rejects them with a
|
|
400 (measured). Deliberately not re-checked here, so this layer keeps a single
|
|
responsibility.
|
|
"""
|
|
|
|
def payload(self, lng):
|
|
return {
|
|
"launch_latitude": 64.1,
|
|
"launch_longitude": lng,
|
|
"launch_datetime": "2026-08-05T12:00:00Z",
|
|
"launch_altitude": 0,
|
|
"ascent_rate": 5,
|
|
"burst_altitude": 30000,
|
|
"descent_rate": 5,
|
|
"profile": "standard_profile",
|
|
}
|
|
|
|
def assert_accepted(self, lng):
|
|
s = PredictionRequestSerializer(data=self.payload(lng))
|
|
self.assertTrue(s.is_valid(), f"lng={lng} rejected: {s.errors}")
|
|
|
|
def test_accepts_a_western_launch(self):
|
|
self.assert_accepted(-51.7)
|
|
|
|
def test_accepts_the_same_meridian_written_unsigned(self):
|
|
self.assert_accepted(308.3)
|
|
|
|
def test_accepts_every_notation_the_predictor_accepts(self):
|
|
# Measured against GET /api/v1/prediction: each of these returns 200, and
|
|
# the pairs below name the same meridian, so each returns the same
|
|
# trajectory as its twin. -180/180 are one meridian; 0/360 are one
|
|
# meridian; -90/270 are one meridian.
|
|
for lng in (-180, 180, 0, 360, -90, 270, -200, -180.0001, 359.999):
|
|
with self.subTest(lng=lng):
|
|
self.assert_accepted(lng)
|
|
|
|
def test_does_not_bound_the_range(self):
|
|
# 1e30 is nonsense, and it is the predictor that says so — it answers 400
|
|
# "lng=1e+30 out of range". Asserting acceptance here is the explicit
|
|
# statement that range is not this layer's job; a bound added here would
|
|
# again risk being narrower than the service behind it.
|
|
self.assert_accepted(1e30)
|
|
|
|
def test_still_refuses_things_that_are_not_numbers(self):
|
|
for lng in ("abc", None, "", []):
|
|
with self.subTest(lng=lng):
|
|
s = PredictionRequestSerializer(data=self.payload(lng))
|
|
self.assertFalse(s.is_valid(), f"lng={lng!r} accepted but is not a number")
|