package stats import ( "context" "strings" "testing" "time" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) func TestNormalizeSourceIPDropsPort(t *testing.T) { tests := map[string]string{ "115.231.168.135:20215": "115.231.168.135", "115.231.168.135": "115.231.168.135", " 115.159.85.149:28316 ": "115.159.85.149", "mqtt://yutong/ytforward/shln/3": "mqtt", "MQTT://YUTONG/topic": "mqtt", "": "", } for input, want := range tests { if got := NormalizeSourceIP(input); got != want { t.Fatalf("NormalizeSourceIP(%q) = %q, want %q", input, got, want) } } } func TestNewSourceIdentityRequiresSourceIP(t *testing.T) { identity, ok := NewSourceIdentity(envelope.ProtocolJT808, "115.231.168.135:20215") if !ok { t.Fatal("NewSourceIdentity() ok = false") } if identity.Protocol != envelope.ProtocolJT808 { t.Fatalf("protocol = %q", identity.Protocol) } if identity.SourceIP != "115.231.168.135" { t.Fatalf("source ip = %q", identity.SourceIP) } if identity.SourceEndpoint != "115.231.168.135:20215" { t.Fatalf("endpoint = %q", identity.SourceEndpoint) } if _, ok := NewSourceIdentity(envelope.ProtocolJT808, ""); ok { t.Fatal("empty endpoint should not produce identity") } } func TestShouldManageDataSourceRequiresPlatformEvidence(t *testing.T) { if ShouldManageDataSource(SourceIdentity{Protocol: envelope.ProtocolJT808, SourceIP: "115.231.168.135"}) { t.Fatal("unclassified source should not be managed") } if !ShouldManageDataSource(SourceIdentity{Protocol: envelope.ProtocolJT808, SourceIP: "115.231.168.135", SourceCode: "g7s"}) { t.Fatal("source_code should make source manageable") } if ShouldManageDataSource(SourceIdentity{Protocol: envelope.ProtocolJT808, SourceIP: "115.231.168.135", SourceKind: "DIRECT"}) { t.Fatal("direct source without platform evidence should not be auto-managed by source IP") } if !ShouldManageDataSource(SourceIdentity{Protocol: envelope.ProtocolJT808, SourceIP: "115.231.168.135", SourceKind: "PLATFORM"}) { t.Fatal("explicit platform source_kind should make source manageable") } if ShouldManageDataSource(SourceIdentity{Protocol: envelope.ProtocolJT808, SourceCode: "g7s"}) { t.Fatal("empty source ip should not be managed") } } func TestSourceKindForDataSourceWriteInfersPlatformEvidence(t *testing.T) { tests := []struct { name string identity SourceIdentity want string }{ { name: "empty evidence", identity: SourceIdentity{Protocol: envelope.ProtocolJT808, SourceIP: "115.231.168.135"}, want: "UNKNOWN", }, { name: "source code", identity: SourceIdentity{Protocol: envelope.ProtocolJT808, SourceIP: "115.231.168.135", SourceCode: "g7s"}, want: "PLATFORM", }, { name: "platform name", identity: SourceIdentity{Protocol: envelope.ProtocolGB32960, SourceIP: "8.134.95.166", PlatformName: "Hyundai"}, want: "PLATFORM", }, { name: "explicit direct wins", identity: SourceIdentity{Protocol: envelope.ProtocolJT808, SourceIP: "115.231.168.135", SourceCode: "direct-import", SourceKind: "DIRECT"}, want: "DIRECT", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := sourceKindForDataSourceWrite(tt.identity); got != tt.want { t.Fatalf("sourceKindForDataSourceWrite() = %q, want %q", got, tt.want) } }) } } func TestUpsertDataSourcePreservesManualFields(t *testing.T) { exec := &recordingExec{} identity := SourceIdentity{ Protocol: envelope.ProtocolJT808, SourceIP: "115.231.168.135", SourceEndpoint: "115.231.168.135:20215", } if err := UpsertDataSource(context.Background(), exec, identity, time.Date(2026, 7, 8, 13, 0, 0, 0, time.UTC)); err != nil { t.Fatalf("UpsertDataSource() error = %v", err) } if len(exec.calls) != 1 { t.Fatalf("exec calls = %d", len(exec.calls)) } sql := exec.calls[0].query for _, want := range []string{ "INSERT INTO vehicle_data_source", "latest_source_endpoint = VALUES(latest_source_endpoint)", "latest_seen_at = GREATEST", } { if !strings.Contains(sql, want) { t.Fatalf("source upsert missing %q: %s", want, sql) } } for _, forbidden := range []string{ "platform_name = VALUES(platform_name)", "trust_priority = VALUES(trust_priority)", "enabled = VALUES(enabled)", "remark = VALUES(remark)", } { if strings.Contains(sql, forbidden) { t.Fatalf("source upsert should preserve manual field %q: %s", forbidden, sql) } } } func TestUpsertDataSourceWritesPlatformKindWhenEvidenceExists(t *testing.T) { exec := &recordingExec{} identity := SourceIdentity{ Protocol: envelope.ProtocolGB32960, SourceIP: "8.134.95.166", SourceEndpoint: "8.134.95.166:32960", SourceCode: "Hyundai", PlatformName: "现代 HTWO", } if err := UpsertDataSource(context.Background(), exec, identity, time.Date(2026, 7, 12, 18, 0, 0, 0, time.UTC)); err != nil { t.Fatalf("UpsertDataSource() error = %v", err) } if len(exec.calls) != 1 { t.Fatalf("exec calls = %d", len(exec.calls)) } if got, want := exec.calls[0].args[5], "PLATFORM"; got != want { t.Fatalf("source_kind arg = %#v, want %q", got, want) } } func TestUpsertDataSourceCanReenableAutoRetiredSourceWithEvidence(t *testing.T) { exec := &recordingExec{} identity := SourceIdentity{ Protocol: envelope.ProtocolJT808, SourceIP: "115.231.168.135", SourceEndpoint: "115.231.168.135:20215", SourceCode: "g7s", PlatformName: "G7s", } if err := UpsertDataSource(context.Background(), exec, identity, time.Date(2026, 7, 12, 18, 0, 0, 0, time.UTC)); err != nil { t.Fatalf("UpsertDataSource() error = %v", err) } sql := exec.calls[0].query for _, want := range []string{ "vehicle_data_source.enabled = 0", "vehicle_data_source.remark LIKE 'auto-retired:%'", "vehicle_data_source.remark = 'auto-reenabled: source evidence restored'", "THEN 'auto-reenabled: source evidence restored'", "THEN 1", } { if !strings.Contains(sql, want) { t.Fatalf("source upsert should re-enable auto-retired source with evidence; missing %q:\n%s", want, sql) } } if strings.Contains(sql, "enabled = VALUES(enabled)") { t.Fatalf("source upsert should not blindly copy enabled from values:\n%s", sql) } if strings.Index(sql, "enabled = CASE") < 0 || strings.Index(sql, "remark = CASE") < 0 || strings.Index(sql, "enabled = CASE") > strings.Index(sql, "remark = CASE") { t.Fatalf("source upsert must restore enabled before updating remark because MySQL evaluates assignments in order:\n%s", sql) } } func TestDataSourceSchemaIncludesStableSourceCode(t *testing.T) { for _, want := range []string{ "source_code VARCHAR(64) NULL", "source_kind VARCHAR(32) NOT NULL DEFAULT 'UNKNOWN'", "KEY idx_protocol_source_code (protocol, source_code)", "KEY idx_protocol_source_kind_seen (protocol, source_kind, latest_seen_at)", } { if !strings.Contains(DataSourceTableSQL, want) { t.Fatalf("data source schema missing %q:\n%s", want, DataSourceTableSQL) } } for _, want := range []string{ "ALTER TABLE vehicle_data_source ADD COLUMN source_code", "ALTER TABLE vehicle_data_source ADD COLUMN source_kind", "ALTER TABLE vehicle_data_source ADD KEY idx_protocol_source_code", "ALTER TABLE vehicle_data_source ADD KEY idx_protocol_source_kind_seen", } { if !containsStatement(DailyMileageAlterSQL, want) { t.Fatalf("data source alter SQL missing %q: %#v", want, DailyMileageAlterSQL) } } } func TestDailyMileageSourceSchemaIncludesQueryIndexes(t *testing.T) { for _, want := range []string{ "KEY idx_protocol_date_selected (protocol, stat_date, is_selected, vin)", "KEY idx_protocol_date_quality (protocol, stat_date, quality_status, vin)", "KEY idx_protocol_date_quality_reason (protocol, stat_date, quality_status, quality_reason, vin)", "KEY idx_source_ip_date (protocol, source_ip, stat_date)", } { if !strings.Contains(DailyMileageSourceTableSQL, want) { t.Fatalf("daily mileage source schema missing %q:\n%s", want, DailyMileageSourceTableSQL) } if !containsStatement(DailyMileageAlterSQL, "ALTER TABLE vehicle_daily_mileage_source ADD "+want) { t.Fatalf("daily mileage source alter SQL missing %q: %#v", want, DailyMileageAlterSQL) } } } func containsStatement(statements []string, fragment string) bool { for _, statement := range statements { if strings.Contains(statement, fragment) { return true } } return false }