Code review fixes: app 2.9.0, server 2.2.0
Telemetry no longer loses queued batches. The server drops a bad value, or a sample without a plausible time, instead of rejecting the batch, and answers 200 with what it dropped. The app sends only values inside the server's ranges (speed as a magnitude, so reversing is fine) and no samples before the head unit's clock is set. Server: - Pairing hands out and accepts a token only after the device is saved; a failed save leaves the request pending. - A paired token serves only the installation id it was paired with (403 otherwise); the shared INGEST_TOKEN still serves any. - Wrong-password lockouts and open pairing requests are limited per client address (last X-Forwarded-For entry), so a stranger cannot lock the owner out; a car starting again replaces its own request. - ABRP forwarding skips samples stamped ahead of the server's clock, which used to stop it until a restart. App: - Settings written to plaintext during a keystore failure move back into encrypted storage; the stored installation id wins. - The installation id is derived from ANDROID_ID on first start, so a reinstall finds its history; a stored or imported id still wins. - The scanned config file is deleted after import (it holds the token). - GPS stops while live telemetry is off or the server unconfigured, and pauses while parked and idle, keeping the fix the car parked with. - Range estimates use the monotonic clock. - One HTTP helper (ServerHttp) for telemetry, sync and pairing. - Last Turkish strings translated; mise logs shows the NilsMG4 tags. Grafana reads InfluxDB with its own read-only token (new grafana-influx Secret, created by hand as described in k8s/README.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/** Moving a keystore-fallback run's settings back into encrypted storage — pure JVM. */
|
||||
public class SecurePrefsTest {
|
||||
|
||||
private static Map<String, String> map(String... kv) {
|
||||
Map<String, String> m = new HashMap<>();
|
||||
for (int i = 0; i < kv.length; i += 2) m.put(kv[i], kv[i + 1]);
|
||||
return m;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aTokenSavedDuringTheFallbackIsKept() {
|
||||
Map<String, String> moves = SecurePrefs.mergePlaintext(
|
||||
map(ServerConfig.KEY_TOKEN, "new-token", ServerConfig.KEY_URL, "https://car.example.com"),
|
||||
map(ServerConfig.KEY_TOKEN, "old-token"));
|
||||
assertEquals("new-token", moves.get(ServerConfig.KEY_TOKEN));
|
||||
assertEquals("https://car.example.com", moves.get(ServerConfig.KEY_URL));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theStoredInstallationIdWins() {
|
||||
Map<String, String> moves = SecurePrefs.mergePlaintext(
|
||||
map(ServerConfig.KEY_INSTALLATION_ID, "made-up-in-the-fallback"),
|
||||
map(ServerConfig.KEY_INSTALLATION_ID, "the-one-history-is-under"));
|
||||
assertFalse(moves.containsKey(ServerConfig.KEY_INSTALLATION_ID));
|
||||
|
||||
// With none stored yet, the fallback's id is the only one there is.
|
||||
moves = SecurePrefs.mergePlaintext(map(ServerConfig.KEY_INSTALLATION_ID, "only-one"), map());
|
||||
assertEquals("only-one", moves.get(ServerConfig.KEY_INSTALLATION_ID));
|
||||
}
|
||||
}
|
||||
@@ -44,4 +44,21 @@ public class ServerConfigTest {
|
||||
assertFalse(ServerConfig.isValidInstallationId("not-a-uuid"));
|
||||
assertFalse(ServerConfig.isValidInstallationId(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theInstallationIdFollowsTheHeadUnit() {
|
||||
String id = ServerConfig.installationIdFor("a1b2c3d4e5f60718");
|
||||
assertTrue(id, ServerConfig.isValidInstallationId(id));
|
||||
// The same head unit gets the same id after a reinstall; another gets another.
|
||||
assertEquals(id, ServerConfig.installationIdFor(" A1B2C3D4E5F60718 "));
|
||||
assertFalse(id.equals(ServerConfig.installationIdFor("0000000000000001")));
|
||||
assertFalse(id.contains("a1b2c3d4e5f60718"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noUsableAndroidIdMeansNoDerivedId() {
|
||||
assertNull(ServerConfig.installationIdFor(null));
|
||||
assertNull(ServerConfig.installationIdFor(""));
|
||||
assertNull(ServerConfig.installationIdFor("9774d56d682e549c"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,4 +311,45 @@ public class TelemetryPayloadTest {
|
||||
assertTrue(summary, summary.startsWith("utc=1700000000"));
|
||||
assertTrue(summary, summary.contains("| omitted: soc,speed,"));
|
||||
}
|
||||
|
||||
// ---------- The server's ranges (server/model.go) ----------
|
||||
|
||||
@Test
|
||||
public void reversingSendsSpeedAsAMagnitude() throws Exception {
|
||||
TelemetryPayload t = new TelemetryPayload(1_700_000_000L);
|
||||
t.speedKmh = -4.2f;
|
||||
assertEquals(4, new JSONObject(t.build()).getInt("speed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void valuesOutsideTheServersRangeAreLeftOut() throws Exception {
|
||||
TelemetryPayload t = new TelemetryPayload(1_700_000_000L);
|
||||
t.soc = 55;
|
||||
t.extTempC = 72f; // plausible to the car's guard, above the server's 70
|
||||
t.hvacSetpointC = 50f; // a HI sentinel, above the server's 40
|
||||
t.rangeKm = 2500;
|
||||
JSONObject json = new JSONObject(t.build());
|
||||
assertEquals(55, json.getInt("soc"));
|
||||
assertFalse(json.has("ext_temp"));
|
||||
assertFalse(json.has("hvac_setpoint"));
|
||||
assertFalse(json.has("est_battery_range"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theAppsLimitsMatchEveryNumericServerField() {
|
||||
String[] serverFields = {"soc", "speed", "power", "est_battery_range", "capacity", "soe",
|
||||
"kwh_charged", "odometer", "lat", "lon", "elevation", "heading", "batt_temp",
|
||||
"cabin_temp", "ext_temp", "hvac_setpoint", "voltage", "current",
|
||||
"tire_pressure_fl", "tire_pressure_fr", "tire_pressure_rl", "tire_pressure_rr"};
|
||||
for (String field : serverFields) {
|
||||
assertTrue(field, TelemetryPayload.SERVER_LIMITS.containsKey(field));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aClockBefore2001IsNotATime() {
|
||||
assertFalse(TelemetryPayload.isPlausibleUtc(0L));
|
||||
assertFalse(TelemetryPayload.isPlausibleUtc(999_999_999L));
|
||||
assertTrue(TelemetryPayload.isPlausibleUtc(1_790_000_000L));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/** Reading the server's answer to a telemetry batch — pure JVM. */
|
||||
public class TelemetryUploaderTest {
|
||||
|
||||
@Test
|
||||
public void theLogSaysWhatTheServerDropped() {
|
||||
assertEquals(", 2 value(s) dropped by the server", TelemetryUploader.droppedNote(
|
||||
new ServerHttp.Response(200, "{\"stored\":3,\"droppedCount\":2,\"dropped\":[]}")));
|
||||
assertEquals("", TelemetryUploader.droppedNote(
|
||||
new ServerHttp.Response(200, "{\"stored\":3,\"droppedCount\":0,\"dropped\":[]}")));
|
||||
// An older server answers 204 without a body.
|
||||
assertEquals("", TelemetryUploader.droppedNote(new ServerHttp.Response(204, "")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user