package com.hawhamburg.micr0bu import com.hawhamburg.micr0bu.domain.denm.DenmParser import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test /** * Tests [DenmParser] against the CiT One Use Case API's documented DENM schema. * * The payload below is the worked example from `CI-CiT-MQTT_API_Documentation-v6-20250221.pdf`, * listing 2.6 (section 2.2.4, "Processed DENM"), reproduced field-for-field. That document is the * contract for this topic, so it is the right thing to pin against - the previous parser was * written before the schema was checked and silently dropped every real DENM for two independent * reasons: the station id is `originatingStationId` (not `stationId`), and `eventPosition` is * itself a GeoJSON Point rather than an object containing one. */ class DenmParserMqttTest { /** Listing 2.6 from the API documentation, with the doc's inline comments removed. */ private val documentedDenm = """ { "type": "v2x-denm", "originatingStationId": 1345267, "sequenceNumber": 1, "detectionTime": "2021-05-11T12:01:02+00:00", "referenceTime": "2021-05-11T12:01:02+00:00", "eventPosition": { "type": "Point", "coordinates": [9.9800230, 53.5560783, 15] }, "relevanceTrafficDirection": "upstreamTraffic", "stationType": "roadSideUnit", "causeCode": "trafficCondition", "subCauseCode": 0 } """.trimIndent() @Test fun `parses the documented DENM payload`() { val denm = DenmParser.parse(documentedDenm, timestamp = 1_787_000_000_000L) assertNotNull("the API's own documented payload must parse", denm) denm!! assertEquals(1_345_267L, denm.stationId) assertEquals(1, denm.sequenceNumber) // GeoJSON is [longitude, latitude, altitude] - getting this order wrong puts a Hamburg // hazard in Somalia, and both values are plausible-looking numbers either way. assertEquals(53.5560783, denm.latitude, 1e-7) assertEquals(9.9800230, denm.longitude, 1e-7) assertEquals(1, denm.causeCode) // trafficCondition assertEquals(0, denm.subCauseCode) assertEquals(15, denm.stationType) // roadSideUnit is 15, not 12 - the enum has a gap assertFalse(denm.isTermination) // 2021-05-11T12:01:02Z assertEquals(1_620_734_462_000L, denm.detectionTimeMs) } @Test fun `termination is signalled by the key being present`() { val terminated = documentedDenm.replace( "\"sequenceNumber\": 1,", "\"sequenceNumber\": 1,\n \"termination\": true,", ) val denm = DenmParser.parse(terminated) assertNotNull(denm) assertTrue(denm!!.isTermination) } @Test fun `a termination shares the dedup key of the event it ends`() { val active = DenmParser.parse(documentedDenm)!! val terminated = DenmParser.parse( documentedDenm.replace( "\"sequenceNumber\": 1,", "\"sequenceNumber\": 1,\n \"termination\": true,", ) )!! // Without this, a cancelled hazard would be filtered out while the active pin it was // meant to cancel stayed on the map forever. assertEquals(active.dedupKey, terminated.dedupKey) } @Test fun `cause code names follow the ETSI spelling the API uses`() { // ETSI's CauseCodeType really does spell it with three n's, and the API follows. val aqua = documentedDenm.replace("\"trafficCondition\"", "\"aquaplannning\"") assertEquals(7, DenmParser.parse(aqua)!!.causeCode) val stationary = documentedDenm.replace("\"trafficCondition\"", "\"stationaryVehicle\"") assertEquals(94, DenmParser.parse(stationary)!!.causeCode) } @Test fun `a payload with no usable position is rejected rather than placed at null island`() { val noPosition = documentedDenm.replace( "\"eventPosition\": { \"type\": \"Point\", \"coordinates\": [9.9800230, 53.5560783, 15] },", "", ) assertNull(DenmParser.parse(noPosition)) } @Test fun `integer causeCode and stationId spellings still parse`() { // The air path and any future firmware-side JSON produce integers; those must keep working. val numeric = """ {"stationId": 42, "causeCode": 94, "subCauseCode": 1, "eventPosition": {"type": "Point", "coordinates": [10.0, 53.5]}} """.trimIndent() val denm = DenmParser.parse(numeric) assertNotNull(denm) assertEquals(42L, denm!!.stationId) assertEquals(94, denm.causeCode) assertEquals(1, denm.subCauseCode) } }