Files
tconnectsync/tests/test_nightscout.py
xannasavinandJames Woglom 758204d4de URL-encode Nightscout date filters instead of retrying with a mangled timestamp
arrow's isoformat() ends a timestamp with its UTC offset, e.g.
'2026-07-16T00:00:00+02:00'. Placed raw into a query string, '+' is a
reserved character that servers decode as a space, so Nightscout receives
'2026-07-16T00:00:00 02:00' and answers 'could not parse as a valid ISO-8601
date'. Any user on a positive UTC offset hits this on every date-filtered
query.

The current workaround retries the request with 'T' replaced by a space
(t_to_space) and treats a failure as 'no previous entry'. That gets a
response, but it works around the symptom: the value is still mangled, every
affected query costs two round-trips, and a genuinely empty result is
indistinguishable from a rejected one.

Percent-encoding the value fixes the cause: the offset survives, the first
request succeeds, and the t_to_space fallback and its retry wrapper are no
longer needed and are removed.

Adds tests for time_range(): that a positive offset is encoded rather than
emitted raw, that the encoded value round-trips back to the original instant,
that negative offsets and 'Z' still parse, and the bounds/no-bounds cases.
There was no coverage of time_range() before. The encoding test fails on
master with '+' present in the query and passes with this change.

Running with this in my EU deployment since May.
2026-07-20 20:50:13 -04:00

69 lines
2.5 KiB
Python

#!/usr/bin/env python3
"""Tests for the Nightscout date-filter query building in time_range()."""
import unittest
import urllib.parse
import arrow
from tconnectsync.nightscout import time_range
class TestTimeRangeEncoding(unittest.TestCase):
"""A positive UTC offset ends an ISO-8601 timestamp with '+02:00'. Placed
raw into a query string, the '+' is a reserved character that servers decode
as a space, so Nightscout receives '2026-07-16T00:00:00 02:00' and rejects
it with "could not parse as a valid ISO-8601 date". Percent-encoding the
value keeps the offset intact."""
def test_positive_offset_is_percent_encoded(self):
start = arrow.get("2026-07-16T00:00:00+02:00")
arg = time_range('created_at', start, None)
self.assertNotIn(
'+', arg,
"A raw '+' in the query string is decoded to a space by the server, "
"mangling the timestamp. Got: %s" % arg,
)
self.assertIn('%2B', arg, "Expected the offset '+' to be encoded as %%2B. Got: %s" % arg)
def test_encoded_value_round_trips_to_the_original_timestamp(self):
"""Decoding the query the way a server would must yield the timestamp
we meant to send."""
start = arrow.get("2026-07-16T00:00:00+02:00")
arg = time_range('created_at', start, None)
value = arg.split('=', 1)[1]
self.assertEqual(
arrow.get(urllib.parse.unquote(value)), start,
"Round-tripping the encoded filter must reproduce the original instant",
)
def test_negative_offset_and_z_suffix_still_parse(self):
"""US pumps report a '-04:00' offset and UTC values end in 'Z'; neither
is ambiguous in a query string, but both must survive encoding."""
for iso in ("2026-07-16T00:00:00-04:00", "2026-07-16T00:00:00Z"):
with self.subTest(iso=iso):
expected = arrow.get(iso)
arg = time_range('created_at', expected, None)
value = arg.split('=', 1)[1]
self.assertEqual(arrow.get(urllib.parse.unquote(value)), expected)
def test_both_bounds_are_emitted(self):
start = arrow.get("2026-07-16T00:00:00+02:00")
end = arrow.get("2026-07-17T00:00:00+02:00")
arg = time_range('created_at', start, end)
self.assertIn('find[created_at][$gte]=', arg)
self.assertIn('find[created_at][$lte]=', arg)
def test_omitted_bounds_produce_no_filter(self):
self.assertEqual(time_range('created_at', None, None), '')
if __name__ == "__main__":
unittest.main()