package platform import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "regexp" "testing" "time" "github.com/DATA-DOG/go-sqlmock" ) func TestVehicleProfileReturnsCompletenessAndMissingFields(t *testing.T) { service := NewService(NewMockStore()) profile, err := service.VehicleProfile(context.Background(), "LB9A32A24R0LS1426") if err != nil { t.Fatal(err) } if profile.Completeness != 100 || len(profile.MissingFields) != 0 || profile.RuntimeSeconds == nil { t.Fatalf("expected complete seeded profile, got %+v", profile) } empty, err := service.VehicleProfile(context.Background(), "LNXNEGRR7SR318212") if err != nil { t.Fatal(err) } if empty.Version != 0 || empty.Completeness != 0 || len(empty.MissingFields) != 8 { t.Fatalf("expected explicit empty profile, got %+v", empty) } } func TestSaveVehicleProfileCreatesAndUsesOptimisticVersion(t *testing.T) { service := NewService(NewMockStore()) runtime := int64(3600) input := VehicleProfileInput{BrandName: "现代", ModelName: "氢燃料重卡", VehicleType: "重卡", CompanyName: "示范物流", OperationStatus: "active", AccessProvider: "车厂平台", FirstAccessAt: "2026-07-01T08:30:00+08:00", RuntimeSeconds: &runtime, Actor: "admin-a"} profile, err := service.SaveVehicleProfile(context.Background(), "LNXNEGRR7SR318212", input) if err != nil { t.Fatal(err) } if profile.Version != 1 || profile.Completeness != 100 || profile.UpdatedBy != "admin-a" { t.Fatalf("unexpected created profile: %+v", profile) } input.Version = 0 _, err = service.SaveVehicleProfile(context.Background(), "LNXNEGRR7SR318212", input) clientErr, ok := asClientError(err) if !ok || clientErr.Code != "VEHICLE_PROFILE_VERSION_CONFLICT" { t.Fatalf("stale write should conflict, got %v", err) } } func TestVehicleProfileValidationRejectsInventedVINAndInvalidValues(t *testing.T) { service := NewService(NewMockStore()) negative := int64(-1) _, err := service.SaveVehicleProfile(context.Background(), "LNOTPRESENT000000", VehicleProfileInput{OperationStatus: "active", Actor: "admin"}) clientErr, ok := asClientError(err) if !ok || clientErr.Code != "VEHICLE_NOT_FOUND" { t.Fatalf("invented VIN should be rejected, got %v", err) } _, err = service.SaveVehicleProfile(context.Background(), "LNXNEGRR7SR318212", VehicleProfileInput{OperationStatus: "flying", RuntimeSeconds: &negative, Actor: "admin"}) clientErr, ok = asClientError(err) if !ok || clientErr.Code != "VEHICLE_PROFILE_STATUS_INVALID" { t.Fatalf("invalid status should be rejected first, got %v", err) } } func TestVehicleProfileSyncIsDryRunnableIdempotentAndVersionSafe(t *testing.T) { service := NewService(NewMockStore()) runtime := int64(7200) request := VehicleProfileSyncRequest{ SourceSystem: " OEM-TSP ", SourceVersion: "snapshot-20260714-01", DryRun: true, Actor: "sync-admin", Items: []VehicleProfileSyncItem{{VIN: " lnxnegrr7sr318212 ", ModelName: "氢燃料重卡", VehicleType: "重卡", CompanyName: "示范物流", OperationStatus: "ACTIVE", AccessProvider: "车厂平台", FirstAccessAt: "2026-07-01T08:30:00+08:00", RuntimeSeconds: &runtime}}, } dryRun, err := service.SyncVehicleProfiles(context.Background(), request) if err != nil { t.Fatal(err) } if !dryRun.DryRun || dryRun.Created != 1 || dryRun.Items[0].VIN != "LNXNEGRR7SR318212" { t.Fatalf("unexpected dry-run result: %+v", dryRun) } profile, err := service.VehicleProfile(context.Background(), "LNXNEGRR7SR318212") if err != nil || profile.Version != 0 { t.Fatalf("dry-run must not persist profile: profile=%+v err=%v", profile, err) } request.DryRun = false created, err := service.SyncVehicleProfiles(context.Background(), request) if err != nil || created.Created != 1 { t.Fatalf("expected created sync: result=%+v err=%v", created, err) } profile, err = service.VehicleProfile(context.Background(), "LNXNEGRR7SR318212") if err != nil || profile.SourceSystem != "oem-tsp" || profile.SourceVersion != request.SourceVersion || profile.Version != 1 { t.Fatalf("unexpected synced profile: %+v err=%v", profile, err) } repeated, err := service.SyncVehicleProfiles(context.Background(), request) if err != nil || repeated.Unchanged != 1 || repeated.Items[0].Status != profileSyncUnchanged { t.Fatalf("same source version should be idempotent: result=%+v err=%v", repeated, err) } request.Items[0].CompanyName = "同版本篡改" conflict, err := service.SyncVehicleProfiles(context.Background(), request) if err != nil || conflict.Conflicted != 1 || conflict.Items[0].Status != profileSyncConflictSourceVersion { t.Fatalf("changed payload under same source version should conflict: result=%+v err=%v", conflict, err) } request.SourceVersion = "snapshot-20260714-02" updated, err := service.SyncVehicleProfiles(context.Background(), request) if err != nil || updated.Updated != 1 || updated.Items[0].ProfileVersion != 2 { t.Fatalf("new source version should update: result=%+v err=%v", updated, err) } } func TestVehicleProfileSyncProtectsManualOwnershipAndReportsMissingVIN(t *testing.T) { service := NewService(NewMockStore()) request := VehicleProfileSyncRequest{ SourceSystem: "fleet-gps", SourceVersion: "42", Actor: "sync-admin", Items: []VehicleProfileSyncItem{ {VIN: "LB9A32A24R0LS1426", ModelName: "外部车型", OperationStatus: "active"}, {VIN: "LNOTPRESENT000000", ModelName: "未知车辆", OperationStatus: "active"}, }, } result, err := service.SyncVehicleProfiles(context.Background(), request) if err != nil { t.Fatal(err) } if result.Conflicted != 1 || result.Missing != 1 || result.Items[0].Status != profileSyncConflictSource || result.Items[1].Status != profileSyncMissingVehicle { t.Fatalf("default policy must preserve manual profile and report missing VIN: %+v", result) } request.ConflictPolicy = profileSyncConflictPolicyOverwrite request.Items = request.Items[:1] overwritten, err := service.SyncVehicleProfiles(context.Background(), request) if err != nil || overwritten.Updated != 1 { t.Fatalf("explicit overwrite should take ownership: result=%+v err=%v", overwritten, err) } profile, _ := service.VehicleProfile(context.Background(), "LB9A32A24R0LS1426") if profile.SourceSystem != "fleet-gps" || profile.ModelName != "外部车型" { t.Fatalf("overwrite did not update source ownership: %+v", profile) } } func TestVehicleProfileSyncValidatesBatchBoundsAndDuplicateVIN(t *testing.T) { service := NewService(NewMockStore()) base := VehicleProfileSyncRequest{SourceSystem: "oem-tsp", SourceVersion: "1", Actor: "admin"} base.Items = []VehicleProfileSyncItem{{VIN: "LNXNEGRR7SR318212"}, {VIN: "lnxnegrr7sr318212"}} _, err := service.SyncVehicleProfiles(context.Background(), base) clientErr, ok := asClientError(err) if !ok || clientErr.Code != "VEHICLE_PROFILE_SYNC_DUPLICATE_VIN" { t.Fatalf("duplicate VIN should fail validation, got %v", err) } base.Items = make([]VehicleProfileSyncItem, vehicleProfileSyncLimit+1) _, err = service.SyncVehicleProfiles(context.Background(), base) clientErr, ok = asClientError(err) if !ok || clientErr.Code != "VEHICLE_PROFILE_SYNC_SIZE_INVALID" { t.Fatalf("oversized batch should fail validation, got %v", err) } } func TestVehicleDetailIncludesProfile(t *testing.T) { service := NewService(NewMockStore()) detail, err := service.VehicleDetail(context.Background(), "LB9A32A24R0LS1426", "") if err != nil { t.Fatal(err) } if detail.Profile == nil || detail.Profile.CompanyName != "岭牛示范车队" || detail.Profile.Completeness != 100 { t.Fatalf("vehicle detail missing master profile: %+v", detail.Profile) } } func TestVehicleProfileHandlersReadAndUpdate(t *testing.T) { handler := NewHandler(NewService(NewMockStore())) read := httptest.NewRecorder() handler.ServeHTTP(read, httptest.NewRequest(http.MethodGet, "/api/v2/vehicles/LNXNEGRR7SR318212/profile", nil)) if read.Code != http.StatusOK || !bytes.Contains(read.Body.Bytes(), []byte(`"completeness":0`)) { t.Fatalf("empty profile read status=%d body=%s", read.Code, read.Body.String()) } update := httptest.NewRecorder() body := bytes.NewBufferString(`{"brandName":"现代","modelName":"氢燃料重卡","vehicleType":"重卡","companyName":"示范物流","operationStatus":"active","accessProvider":"车厂平台","firstAccessAt":"2026-07-01T08:30","runtimeSeconds":3600,"version":0}`) handler.ServeHTTP(update, httptest.NewRequest(http.MethodPut, "/api/v2/vehicles/LNXNEGRR7SR318212/profile", body)) if update.Code != http.StatusOK { t.Fatalf("profile update status=%d body=%s", update.Code, update.Body.String()) } var response struct { Data VehicleProfile `json:"data"` } if err := json.Unmarshal(update.Body.Bytes(), &response); err != nil { t.Fatal(err) } if response.Data.Version != 1 || response.Data.Completeness != 100 || response.Data.UpdatedBy != "system" { t.Fatalf("unexpected handler profile: %+v", response.Data) } } func TestVehicleProfileSyncHandlerUsesAuthenticatedActor(t *testing.T) { handler := NewHandler(NewService(NewMockStore())) recorder := httptest.NewRecorder() body := bytes.NewBufferString(`{"sourceSystem":"oem-tsp","sourceVersion":"v1","items":[{"vin":"LNXNEGRR7SR318212","modelName":"氢燃料重卡","operationStatus":"active"}]}`) handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodPost, "/api/v2/vehicle-profiles/sync", body)) if recorder.Code != http.StatusOK || !bytes.Contains(recorder.Body.Bytes(), []byte(`"created":1`)) { t.Fatalf("profile sync status=%d body=%s", recorder.Code, recorder.Body.String()) } profile, _ := handler.service.VehicleProfile(context.Background(), "LNXNEGRR7SR318212") if profile.UpdatedBy != "system" { t.Fatalf("handler must override body actor with authenticated actor, got %+v", profile) } } func TestProductionVehicleProfileCreateIsTransactionalAndAudited(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() store := NewProductionStore(db, nil, "") now := time.Now() runtime := int64(7200) mock.ExpectQuery(regexp.QuoteMeta(`SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name IN ('vehicle_profile','vehicle_profile_audit')`)).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(2)) mock.ExpectBegin() mock.ExpectQuery(regexp.QuoteMeta(`SELECT version FROM vehicle_profile WHERE vin=? FOR UPDATE`)).WithArgs("VIN001").WillReturnRows(sqlmock.NewRows([]string{"version"})) mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO vehicle_profile(vin,brand_name,model_name,vehicle_type,company_name,operation_status,access_provider,first_access_at,runtime_seconds,source_system,source_version,synced_at,version,updated_by) VALUES(?,?,?,?,?,?,?,?,?, 'manual','',NULL,1,?)`)).WithArgs("VIN001", "现代", "示范车型", "重卡", "示范物流", "active", "车厂平台", sqlmock.AnyArg(), runtime, "admin-a").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO vehicle_profile_audit(vin,profile_version,actor,action,snapshot_json) VALUES(?,?,?,?,?)`)).WithArgs("VIN001", 1, "admin-a", "create", sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() columns := []string{"vin", "brand_name", "model_name", "vehicle_type", "company_name", "operation_status", "access_provider", "first_access_at", "runtime_seconds", "source_system", "source_version", "synced_at", "version", "updated_by", "updated_at"} mock.ExpectQuery(regexp.QuoteMeta(vehicleProfileSelect + `WHERE vin=?`)).WithArgs("VIN001").WillReturnRows(sqlmock.NewRows(columns).AddRow("VIN001", "现代", "示范车型", "重卡", "示范物流", "active", "车厂平台", now, runtime, "manual", "", nil, 1, "admin-a", now)) profile, err := store.SaveVehicleProfile(context.Background(), "VIN001", VehicleProfileInput{BrandName: "现代", ModelName: "示范车型", VehicleType: "重卡", CompanyName: "示范物流", OperationStatus: "active", AccessProvider: "车厂平台", FirstAccessAt: now.Format(time.RFC3339), RuntimeSeconds: &runtime, Actor: "admin-a"}) if err != nil { t.Fatal(err) } if profile.Version != 1 || profile.RuntimeSeconds == nil || *profile.RuntimeSeconds != runtime { t.Fatalf("unexpected stored profile: %+v", profile) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestProductionVehicleProfileSyncCreatesAndAuditsInOneTransaction(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() store := NewProductionStore(db, nil, "") runtime := int64(3600) request := VehicleProfileSyncRequest{ SourceSystem: "oem-tsp", SourceVersion: "snapshot-1", Actor: "sync-admin", Items: []VehicleProfileSyncItem{{VIN: "VIN001", BrandName: "现代", ModelName: "示范车型", VehicleType: "重卡", CompanyName: "示范物流", OperationStatus: "active", AccessProvider: "车厂平台", FirstAccessAt: "2026-07-01T08:30:00+08:00", RuntimeSeconds: &runtime}}, } mock.ExpectQuery(regexp.QuoteMeta(`SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name IN ('vehicle_profile','vehicle_profile_audit')`)).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(2)) mock.ExpectBegin() mock.ExpectQuery(regexp.QuoteMeta(`SELECT DISTINCT vin FROM vehicle_identity_binding WHERE vin IN (?)`)).WithArgs("VIN001").WillReturnRows(sqlmock.NewRows([]string{"vin"}).AddRow("VIN001")) mock.ExpectQuery(regexp.QuoteMeta(vehicleProfileSelect + `WHERE vin=? FOR UPDATE`)).WithArgs("VIN001").WillReturnRows(sqlmock.NewRows([]string{"vin", "brand_name", "model_name", "vehicle_type", "company_name", "operation_status", "access_provider", "first_access_at", "runtime_seconds", "source_system", "source_version", "synced_at", "version", "updated_by", "updated_at"})) mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO vehicle_profile(vin,brand_name,model_name,vehicle_type,company_name,operation_status,access_provider,first_access_at,runtime_seconds,source_system,source_version,synced_at,version,updated_by) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,1,?)`)).WithArgs("VIN001", "现代", "示范车型", "重卡", "示范物流", "active", "车厂平台", sqlmock.AnyArg(), runtime, "oem-tsp", "snapshot-1", sqlmock.AnyArg(), "sync-admin").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO vehicle_profile_audit(vin,profile_version,actor,action,snapshot_json) VALUES(?,?,?,?,?)`)).WithArgs("VIN001", 1, "sync-admin", "sync_created", sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() result, err := store.SyncVehicleProfiles(context.Background(), request) if err != nil { t.Fatal(err) } if result.Created != 1 || result.Items[0].ProfileVersion != 1 { t.Fatalf("unexpected sync result: %+v", result) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }