🚧 WIP
This commit is contained in:
+118
-33
@@ -34,8 +34,8 @@ class TestNormalizeImei:
|
||||
)
|
||||
def test_valid(self, value: str, expected: str) -> None:
|
||||
result = process_message(f"/up/{value}", b"{}", IMEI_MAP)
|
||||
assert result is not None
|
||||
assert result.imei == expected
|
||||
assert len(result) == 1
|
||||
assert result[0].imei == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
@@ -50,53 +50,138 @@ class TestNormalizeImei:
|
||||
],
|
||||
)
|
||||
def test_invalid(self, value: str) -> None:
|
||||
assert process_message(f"/up/{value}", b"{}", IMEI_MAP) is None
|
||||
assert process_message(f"/up/{value}", b"{}", IMEI_MAP) == []
|
||||
|
||||
|
||||
class TestProcessMessage:
|
||||
def test_injects_imei_and_mapped_device(self) -> None:
|
||||
payload = json.dumps({"temperature": 21.5}).encode()
|
||||
result = process_message("/vivab/350123456789012", payload, IMEI_MAP)
|
||||
assert result == InjectionResult(
|
||||
imei="350123456789012",
|
||||
device="Greenhouse sensor A",
|
||||
payload={"temperature": 21.5, "imei": "350123456789012", "device": "Greenhouse sensor A"},
|
||||
)
|
||||
assert result == [
|
||||
InjectionResult(
|
||||
imei="350123456789012",
|
||||
device="Greenhouse sensor A",
|
||||
payload={"temperature": 21.5, "imei": "350123456789012", "device": "Greenhouse sensor A"},
|
||||
)
|
||||
]
|
||||
|
||||
def test_unmapped_imei_falls_back_to_imei(self) -> None:
|
||||
payload = json.dumps({"temperature": 21.5}).encode()
|
||||
result = process_message("/vivab/356938035643809", payload, IMEI_MAP)
|
||||
assert result is not None
|
||||
assert result.device == "356938035643809"
|
||||
assert result.payload["device"] == "356938035643809"
|
||||
assert len(result) == 1
|
||||
assert result[0].device == "356938035643809"
|
||||
assert result[0].payload["device"] == "356938035643809"
|
||||
|
||||
def test_overwrites_existing_keys(self) -> None:
|
||||
payload = json.dumps({"imei": "fake", "device": "fake", "seq": 1}).encode()
|
||||
result = process_message("/up/490154203237518", payload, IMEI_MAP)
|
||||
assert result is not None
|
||||
assert result.payload["imei"] == "490154203237518"
|
||||
assert result.payload["device"] == "Water tank sensor B"
|
||||
assert len(result) == 1
|
||||
assert result[0].payload["imei"] == "490154203237518"
|
||||
assert result[0].payload["device"] == "Water tank sensor B"
|
||||
|
||||
def test_does_not_mutate_caller_payload(self) -> None:
|
||||
original = {"seq": 1}
|
||||
result = process_message("/up/350123456789012", json.dumps(original).encode(), IMEI_MAP)
|
||||
assert result is not None
|
||||
assert len(result) == 1
|
||||
assert original == {"seq": 1}
|
||||
|
||||
def test_invalid_json_returns_none(self) -> None:
|
||||
assert process_message("/up/350123456789012", b"not json", IMEI_MAP) is None
|
||||
def test_invalid_json_returns_empty(self) -> None:
|
||||
assert process_message("/up/350123456789012", b"not json", IMEI_MAP) == []
|
||||
|
||||
def test_non_object_json_returns_none(self) -> None:
|
||||
assert process_message("/up/350123456789012", b"[1,2,3]", IMEI_MAP) is None
|
||||
def test_scalar_json_returns_empty(self) -> None:
|
||||
assert process_message("/up/350123456789012", b"\"just a string\"", IMEI_MAP) == []
|
||||
|
||||
def test_array_without_objects_returns_empty(self) -> None:
|
||||
assert process_message("/up/350123456789012", b"[1,2,3]", IMEI_MAP) == []
|
||||
|
||||
def test_empty_array_returns_empty(self) -> None:
|
||||
assert process_message("/up/350123456789012", b"[]", IMEI_MAP) == []
|
||||
|
||||
def test_custom_keys(self) -> None:
|
||||
payload = json.dumps({"seq": 1}).encode()
|
||||
result = process_message(
|
||||
"/up/350123456789012", payload, IMEI_MAP, imei_key="id", device_key="name"
|
||||
)
|
||||
assert result is not None
|
||||
assert result.payload["id"] == "350123456789012"
|
||||
assert result.payload["name"] == "Greenhouse sensor A"
|
||||
assert len(result) == 1
|
||||
assert result[0].payload["id"] == "350123456789012"
|
||||
assert result[0].payload["name"] == "Greenhouse sensor A"
|
||||
|
||||
|
||||
class TestProcessMessageArray:
|
||||
"""JSON arrays of objects: one InjectionResult (and one DB row) per object."""
|
||||
|
||||
def test_single_object_array(self) -> None:
|
||||
payload = json.dumps([{"temperature": 21.5}]).encode()
|
||||
result = process_message("/up/350123456789012", payload, IMEI_MAP)
|
||||
assert len(result) == 1
|
||||
assert result[0].imei == "350123456789012"
|
||||
assert result[0].payload == {
|
||||
"temperature": 21.5,
|
||||
"imei": "350123456789012",
|
||||
"device": "Greenhouse sensor A",
|
||||
}
|
||||
|
||||
def test_array_of_objects(self) -> None:
|
||||
payload = json.dumps(
|
||||
[
|
||||
{"seq": 1, "temperature": 20.0},
|
||||
{"seq": 2, "temperature": 21.0},
|
||||
{"seq": 3, "temperature": 22.0},
|
||||
]
|
||||
).encode()
|
||||
results = process_message("/up/350123456789012", payload, IMEI_MAP)
|
||||
assert len(results) == 3
|
||||
assert [r.payload["seq"] for r in results] == [1, 2, 3]
|
||||
# every array element got its own imei/device injection
|
||||
assert all(r.imei == "350123456789012" for r in results)
|
||||
assert all(r.device == "Greenhouse sensor A" for r in results)
|
||||
assert all(r.payload["device"] == "Greenhouse sensor A" for r in results)
|
||||
|
||||
def test_per_element_payload_time(self) -> None:
|
||||
payload = json.dumps(
|
||||
[
|
||||
{"timestamp": "2026-08-31T09:59:58Z"},
|
||||
{"temperature": 21.5}, # no timestamp -> None
|
||||
{"timestamp": 1756646096},
|
||||
]
|
||||
).encode()
|
||||
results = process_message("/up/350123456789012", payload, IMEI_MAP)
|
||||
assert len(results) == 3
|
||||
assert results[0].payload_time == datetime(2026, 8, 31, 9, 59, 58, tzinfo=timezone.utc)
|
||||
assert results[1].payload_time is None # still stored, just without payload_time
|
||||
assert results[2].payload_time == datetime.fromtimestamp(1756646096, tz=timezone.utc)
|
||||
|
||||
def test_mixed_array_skips_non_objects(self) -> None:
|
||||
payload = json.dumps([{"seq": 1}, "noise", 42, None, {"seq": 2}]).encode()
|
||||
results = process_message("/up/350123456789012", payload, IMEI_MAP)
|
||||
assert [r.payload["seq"] for r in results] == [1, 2]
|
||||
|
||||
def test_nested_arrays_not_flattened(self) -> None:
|
||||
payload = json.dumps([{"seq": 1}, [{"seq": 2}]]).encode()
|
||||
results = process_message("/up/350123456789012", payload, IMEI_MAP)
|
||||
# only top-level objects are messages; the nested array is skipped
|
||||
assert [r.payload["seq"] for r in results] == [1]
|
||||
|
||||
def test_array_overwrites_existing_keys(self) -> None:
|
||||
payload = json.dumps([{"imei": "fake", "device": "fake", "seq": 1}]).encode()
|
||||
results = process_message("/up/490154203237518", payload, IMEI_MAP)
|
||||
assert len(results) == 1
|
||||
assert results[0].payload["imei"] == "490154203237518"
|
||||
assert results[0].payload["device"] == "Water tank sensor B"
|
||||
|
||||
def test_array_with_unmapped_imei_falls_back_to_imei(self) -> None:
|
||||
payload = json.dumps([{"seq": 1}, {"seq": 2}]).encode()
|
||||
results = process_message("/up/356938035643809", payload, IMEI_MAP)
|
||||
assert len(results) == 2
|
||||
assert all(r.device == "356938035643809" for r in results)
|
||||
|
||||
def test_array_does_not_mutate_caller_objects(self) -> None:
|
||||
original = [{"seq": 1}]
|
||||
results = process_message(
|
||||
"/up/350123456789012", json.dumps(original).encode(), IMEI_MAP
|
||||
)
|
||||
assert len(results) == 1
|
||||
assert original == [{"seq": 1}]
|
||||
|
||||
|
||||
class TestParsePayloadTime:
|
||||
@@ -139,36 +224,36 @@ class TestPayloadTimeExtraction:
|
||||
def test_timestamp_extracted_from_payload(self) -> None:
|
||||
payload = json.dumps({"temperature": 21.5, "timestamp": "2026-08-31T09:59:58Z"}).encode()
|
||||
result = process_message("/up/350123456789012", payload, IMEI_MAP)
|
||||
assert result is not None
|
||||
assert result.payload_time == datetime(2026, 8, 31, 9, 59, 58, tzinfo=timezone.utc)
|
||||
assert len(result) == 1
|
||||
assert result[0].payload_time == datetime(2026, 8, 31, 9, 59, 58, tzinfo=timezone.utc)
|
||||
# the original payload value stays untouched
|
||||
assert result.payload["timestamp"] == "2026-08-31T09:59:58Z"
|
||||
assert result[0].payload["timestamp"] == "2026-08-31T09:59:58Z"
|
||||
|
||||
def test_first_parseable_key_wins(self) -> None:
|
||||
payload = json.dumps({"timestamp": "garbage", "time": 1756646096}).encode()
|
||||
result = process_message("/up/350123456789012", payload, IMEI_MAP)
|
||||
assert result is not None
|
||||
assert result.payload_time == datetime.fromtimestamp(1756646096, tz=timezone.utc)
|
||||
assert len(result) == 1
|
||||
assert result[0].payload_time == datetime.fromtimestamp(1756646096, tz=timezone.utc)
|
||||
|
||||
def test_custom_time_keys(self) -> None:
|
||||
payload = json.dumps({"measured_at": "2026-08-31T09:59:58Z"}).encode()
|
||||
result = process_message(
|
||||
"/up/350123456789012", payload, IMEI_MAP, time_keys=("measured_at",)
|
||||
)
|
||||
assert result is not None
|
||||
assert result.payload_time == datetime(2026, 8, 31, 9, 59, 58, tzinfo=timezone.utc)
|
||||
assert len(result) == 1
|
||||
assert result[0].payload_time == datetime(2026, 8, 31, 9, 59, 58, tzinfo=timezone.utc)
|
||||
|
||||
def test_missing_timestamp_yields_none_not_skip(self) -> None:
|
||||
payload = json.dumps({"temperature": 21.5}).encode()
|
||||
result = process_message("/up/350123456789012", payload, IMEI_MAP)
|
||||
assert result is not None # message still stored
|
||||
assert result.payload_time is None
|
||||
assert len(result) == 1 # message still stored
|
||||
assert result[0].payload_time is None
|
||||
|
||||
def test_unparseable_timestamp_yields_none_not_skip(self) -> None:
|
||||
payload = json.dumps({"timestamp": "yesterday-ish"}).encode()
|
||||
result = process_message("/up/350123456789012", payload, IMEI_MAP)
|
||||
assert result is not None
|
||||
assert result.payload_time is None
|
||||
assert len(result) == 1
|
||||
assert result[0].payload_time is None
|
||||
|
||||
|
||||
class TestLoadImeiMap:
|
||||
|
||||
Reference in New Issue
Block a user