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:
+38
-16
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -12,6 +13,9 @@ const (
|
||||
testNowS = 1_790_000_000
|
||||
)
|
||||
|
||||
// testNow is the sample time the telemetry tests send, as "now" for the clock-ahead check.
|
||||
var testNow = time.Unix(testNowS, 0)
|
||||
|
||||
func isValidationError(err error) bool {
|
||||
var v *ValidationError
|
||||
return errors.As(err, &v)
|
||||
@@ -125,7 +129,7 @@ func telemetryBody(samples string) []byte {
|
||||
|
||||
func TestTelemetryKeepsOnlyWhatWasSent(t *testing.T) {
|
||||
b, err := validateTelemetryBatch(telemetryBody(
|
||||
`{"utc":1790000000,"soc":81,"power":-12.5,"speed":0,"lat":52.1,"lon":7.6,"is_charging":1,"is_dcfc":false}`))
|
||||
`{"utc":1790000000,"soc":81,"power":-12.5,"speed":0,"lat":52.1,"lon":7.6,"is_charging":1,"is_dcfc":false}`), testNow)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -146,7 +150,7 @@ func TestTelemetryKeepsOnlyWhatWasSent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTelemetryDropsUnknownFields(t *testing.T) {
|
||||
b, err := validateTelemetryBatch(telemetryBody(`{"utc":1790000000,"future_field":3}`))
|
||||
b, err := validateTelemetryBatch(telemetryBody(`{"utc":1790000000,"future_field":3}`), testNow)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -155,31 +159,49 @@ func TestTelemetryDropsUnknownFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelemetryRejectsBadSamples(t *testing.T) {
|
||||
for name, sample := range map[string]string{
|
||||
"milliseconds": `{"utc":1790000000000}`,
|
||||
"fractional utc": `{"utc":1790000000.5}`,
|
||||
"soc above 100": `{"utc":1790000000,"soc":101}`,
|
||||
"string speed": `{"utc":1790000000,"speed":"fast"}`,
|
||||
"flag 2": `{"utc":1790000000,"is_parked":2}`,
|
||||
"half position": `{"utc":1790000000,"lat":52.1}`,
|
||||
"not an object": `5`,
|
||||
func TestTelemetryDropsBadValuesAndKeepsTheRest(t *testing.T) {
|
||||
for name, c := range map[string]struct {
|
||||
sample string
|
||||
dropped string
|
||||
kept bool // the sample itself survives
|
||||
}{
|
||||
"milliseconds": {`{"utc":1790000000000,"soc":5}`, "samples[0].utc", false},
|
||||
"fractional utc": {`{"utc":1790000000.5,"soc":5}`, "samples[0].utc", false},
|
||||
"clock unset": {`{"utc":0,"soc":5}`, "samples[0].utc", false},
|
||||
"clock a day ahead": {`{"utc":1790086400,"soc":5}`, "samples[0].utc", false},
|
||||
"not an object": {`5`, "samples[0]", false},
|
||||
"soc above 100": {`{"utc":1790000000,"soc":101,"speed":20}`, "samples[0].soc", true},
|
||||
"reversing": {`{"utc":1790000000,"soc":5,"speed":-4}`, "samples[0].speed", true},
|
||||
"string speed": {`{"utc":1790000000,"soc":5,"speed":"fast"}`, "samples[0].speed", true},
|
||||
"flag 2": {`{"utc":1790000000,"soc":5,"is_parked":2}`, "samples[0].is_parked", true},
|
||||
"half position": {`{"utc":1790000000,"soc":5,"lat":52.1}`, "samples[0].lat/lon", true},
|
||||
} {
|
||||
if _, err := validateTelemetryBatch(telemetryBody(sample)); !isValidationError(err) {
|
||||
t.Errorf("%s: want ValidationError, got %v", name, err)
|
||||
b, err := validateTelemetryBatch(telemetryBody(c.sample), testNow)
|
||||
if err != nil {
|
||||
t.Errorf("%s: the batch was rejected: %v", name, err)
|
||||
continue
|
||||
}
|
||||
if len(b.Dropped) != 1 || b.Dropped[0] != c.dropped {
|
||||
t.Errorf("%s: dropped %v, want %s", name, b.Dropped, c.dropped)
|
||||
}
|
||||
if (len(b.Samples) == 1) != c.kept {
|
||||
t.Errorf("%s: %d samples kept", name, len(b.Samples))
|
||||
}
|
||||
if c.kept && b.Samples[0].Numbers["soc"] != 5 && name != "soc above 100" {
|
||||
t.Errorf("%s: the valid soc was lost", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelemetryRejectsEmptyAndOversizedBatches(t *testing.T) {
|
||||
if _, err := validateTelemetryBatch(telemetryBody("")); !isValidationError(err) {
|
||||
if _, err := validateTelemetryBatch(telemetryBody(""), testNow); !isValidationError(err) {
|
||||
t.Error("empty batch accepted")
|
||||
}
|
||||
many := strings.TrimSuffix(strings.Repeat(`{"utc":1790000000},`, 501), ",")
|
||||
if _, err := validateTelemetryBatch(telemetryBody(many)); !isValidationError(err) {
|
||||
if _, err := validateTelemetryBatch(telemetryBody(many), testNow); !isValidationError(err) {
|
||||
t.Error("501 samples accepted")
|
||||
}
|
||||
if _, err := validateTelemetryBatch([]byte(`{"installationId":"nope","samples":[{"utc":1790000000}]}`)); !isValidationError(err) {
|
||||
if _, err := validateTelemetryBatch([]byte(`{"installationId":"nope","samples":[{"utc":1790000000}]}`), testNow); !isValidationError(err) {
|
||||
t.Error("bad installation id accepted")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user