package identity import ( "context" "errors" "os" "path/filepath" "testing" "github.com/DATA-DOG/go-sqlmock" "github.com/xuri/excelize/v2" ) func TestReadMappingDirectoryExtractsPhoneAndPlate(t *testing.T) { dir := t.TempDir() writeWorkbook(t, filepath.Join(dir, "G7s", "宇速全量.xlsx"), [][]string{ {"车牌号", "sim卡号", "设备号"}, {"粤AG18312", "013307795425", "DEV001"}, }) writeWorkbook(t, filepath.Join(dir, "东方北斗", "无标题0703.xlsx"), [][]string{ {"沪A01559F", "64341233712"}, }) if err := os.MkdirAll(filepath.Join(dir, "信达"), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } if err := os.WriteFile(filepath.Join(dir, "信达", "旧格式.xls"), []byte("legacy xls"), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } if err := os.WriteFile(filepath.Join(dir, "信达", "说明.txt"), []byte("ignored"), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } records, report, err := ReadMappingDirectory(dir) if err != nil { t.Fatalf("ReadMappingDirectory() error = %v", err) } if report.Files != 2 { t.Fatalf("files = %d, report=%#v", report.Files, report) } if report.UnsupportedFiles != 1 || len(report.UnsupportedItems) != 1 { t.Fatalf("unsupported files = %d, items=%#v", report.UnsupportedFiles, report.UnsupportedItems) } if got := report.UnsupportedItems[0]; got.Ext != ".xls" || got.Reason == "" { t.Fatalf("unsupported item = %#v", got) } sources := map[string]MappingSourceScanReport{} for _, source := range report.Sources { sources[source.SourceCode] = source } if got := sources["g7s"]; got.Files != 1 || got.Sheets != 1 || got.PhoneRecords != 1 || got.PlateRecords != 1 || got.Records != 2 { t.Fatalf("g7s source report = %#v", got) } if got := sources["dongfang_beidou"]; got.Files != 1 || got.Sheets != 1 || got.PhoneRecords != 1 || got.PlateRecords != 1 || got.Records != 2 { t.Fatalf("dongfang source report = %#v", got) } var phoneSeen bool var plateSeen bool var headerlessSeen bool for _, record := range records { if record.SourceCode == "g7s" && record.IdentifierType == IdentifierTypeJT808Phone && record.IdentifierValue == "13307795425" && record.Plate == "粤AG18312" { phoneSeen = true } if record.SourceCode == "g7s" && record.IdentifierType == IdentifierTypePlate && record.IdentifierValue == "粤AG18312" { plateSeen = true } if record.SourceCode == "dongfang_beidou" && record.IdentifierType == IdentifierTypeJT808Phone && record.IdentifierValue == "64341233712" && record.Plate == "沪A01559F" { headerlessSeen = true } } if !phoneSeen || !plateSeen || !headerlessSeen { t.Fatalf("records missing expected mappings: %#v", records) } } func TestImportMappingRecordsDryRunResolvesVINFromLegacyPlate(t *testing.T) { db, mock := newMockDB(t) defer db.Close() mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE plate = \\?"). WithArgs("粤AG18312"). WillReturnRows(sqlmock.NewRows([]string{"vin"}).AddRow("LB9A32A22P0LS1230")) mock.ExpectQuery("SELECT vin FROM vehicle_identifier"). WithArgs("JT808", "g7s", IdentifierTypeJT808Phone, "13307795425"). WillReturnRows(sqlmock.NewRows([]string{"vin"})) report, err := ImportMappingRecords(context.Background(), db, []MappingRecord{ { SourceCode: "g7s", SourceName: "G7s", Protocol: "JT808", IdentifierType: IdentifierTypeJT808Phone, IdentifierValue: "013307795425", Plate: "粤AG18312", OEM: "G7s", }, }, MappingScanReport{}, MappingImportOptions{LegacyTable: "vehicle_identity_binding"}) if err != nil { t.Fatalf("ImportMappingRecords() error = %v", err) } if report.Resolved != 1 || report.WouldInsert != 1 || report.Unresolved != 0 { t.Fatalf("report = %#v", report) } if len(report.SourceResults) != 1 { t.Fatalf("source results = %#v, want one source", report.SourceResults) } if got := report.SourceResults[0]; got.SourceCode != "g7s" || got.Records != 1 || got.Deduplicated != 1 || got.Resolved != 1 || got.WouldInsert != 1 { t.Fatalf("source result = %#v", got) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func TestImportMappingRecordsReportsSourceResults(t *testing.T) { db, mock := newMockDB(t) defer db.Close() mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE plate = \\?"). WithArgs("粤AG18312"). WillReturnRows(sqlmock.NewRows([]string{"vin"}).AddRow("LB9A32A22P0LS1230")) mock.ExpectQuery("SELECT vin FROM vehicle_identifier"). WithArgs("JT808", "g7s", IdentifierTypeJT808Phone, "13307795425"). WillReturnRows(sqlmock.NewRows([]string{"vin"})) mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE plate = \\?"). WithArgs("粤B00000"). WillReturnRows(sqlmock.NewRows([]string{"vin"})) mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE phone = \\?"). WithArgs("14400000000"). WillReturnRows(sqlmock.NewRows([]string{"vin"})) report, err := ImportMappingRecords(context.Background(), db, []MappingRecord{ { SourceCode: "g7s", SourceName: "G7s", Protocol: "JT808", IdentifierType: IdentifierTypeJT808Phone, IdentifierValue: "13307795425", Plate: "粤AG18312", OEM: "G7s", }, { SourceCode: "g7s", SourceName: "G7s", Protocol: "JT808", IdentifierType: IdentifierTypeJT808Phone, IdentifierValue: "13307795425", Plate: "粤B99999", OEM: "G7s", }, { SourceCode: "xinda", SourceName: "信达", Protocol: "JT808", IdentifierType: IdentifierTypeJT808Phone, IdentifierValue: "14400000000", Plate: "粤B00000", OEM: "信达", }, }, MappingScanReport{}, MappingImportOptions{LegacyTable: "vehicle_identity_binding"}) if err != nil { t.Fatalf("ImportMappingRecords() error = %v", err) } if report.Records != 3 || report.Deduplicated != 2 || report.Resolved != 1 || report.Unresolved != 1 || report.Conflicts != 1 || report.WouldInsert != 1 { t.Fatalf("report = %#v", report) } got := map[string]MappingSourceImportStat{} for _, source := range report.SourceResults { got[source.SourceCode] = source } if source := got["g7s"]; source.Records != 2 || source.Deduplicated != 1 || source.Resolved != 1 || source.Conflicts != 1 || source.WouldInsert != 1 { t.Fatalf("g7s source result = %#v", source) } if source := got["xinda"]; source.Records != 1 || source.Deduplicated != 1 || source.Unresolved != 1 { t.Fatalf("xinda source result = %#v", source) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func TestImportMappingRecordsMergesDuplicateIdentifierDetails(t *testing.T) { db, mock := newMockDB(t) defer db.Close() mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE plate = \\?"). WithArgs("粤AG18312"). WillReturnRows(sqlmock.NewRows([]string{"vin"}).AddRow("LB9A32A22P0LS1230")) mock.ExpectQuery("SELECT vin FROM vehicle_identifier"). WithArgs("JT808", "g7s", IdentifierTypeJT808Phone, "13307795425"). WillReturnRows(sqlmock.NewRows([]string{"vin"})) report, err := ImportMappingRecords(context.Background(), db, []MappingRecord{ { File: "G7s/no-plate.xlsx", SourceCode: "g7s", SourceName: "G7s", Protocol: "JT808", IdentifierType: IdentifierTypeJT808Phone, IdentifierValue: "13307795425", OEM: "G7s", }, { File: "G7s/with-plate.xlsx", SourceCode: "g7s", SourceName: "G7s", Protocol: "JT808", IdentifierType: IdentifierTypeJT808Phone, IdentifierValue: "13307795425", Plate: "粤AG18312", OEM: "G7s", }, }, MappingScanReport{}, MappingImportOptions{LegacyTable: "vehicle_identity_binding"}) if err != nil { t.Fatalf("ImportMappingRecords() error = %v", err) } if report.Records != 2 || report.Deduplicated != 1 || report.Resolved != 1 || report.WouldInsert != 1 || report.Unresolved != 0 { t.Fatalf("report = %#v", report) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func TestImportMappingRecordsApplyCommitsSingleTransaction(t *testing.T) { db, mock := newMockDB(t) defer db.Close() mock.ExpectBegin() mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE plate = \\?"). WithArgs("粤AG18312"). WillReturnRows(sqlmock.NewRows([]string{"vin"}).AddRow("LB9A32A22P0LS1230")) mock.ExpectQuery("SELECT vin FROM vehicle_identifier"). WithArgs("JT808", "g7s", IdentifierTypeJT808Phone, "13307795425"). WillReturnRows(sqlmock.NewRows([]string{"vin"})) mock.ExpectExec("INSERT INTO vehicle"). WithArgs("LB9A32A22P0LS1230", "粤AG18312", "G7s"). WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("INSERT INTO vehicle_identifier"). WithArgs("JT808", "g7s", IdentifierTypeJT808Phone, "13307795425", "LB9A32A22P0LS1230", "粤AG18312", "G7s", "13307795425", "G7s/example.xlsx"). WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectCommit() report, err := ImportMappingRecords(context.Background(), db, []MappingRecord{ { File: "G7s/example.xlsx", SourceCode: "g7s", SourceName: "G7s", Protocol: "JT808", IdentifierType: IdentifierTypeJT808Phone, IdentifierValue: "13307795425", RawValue: "13307795425", Plate: "粤AG18312", OEM: "G7s", }, }, MappingScanReport{}, MappingImportOptions{ Apply: true, LegacyTable: "vehicle_identity_binding", }) if err != nil { t.Fatalf("ImportMappingRecords() error = %v", err) } if report.Inserted != 1 || report.Resolved != 1 || report.WouldInsert != 0 { t.Fatalf("report = %#v", report) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func TestImportMappingRecordsApplyRollsBackOnWriteError(t *testing.T) { db, mock := newMockDB(t) defer db.Close() errWrite := errors.New("insert vehicle failed") mock.ExpectBegin() mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE plate = \\?"). WithArgs("粤AG18312"). WillReturnRows(sqlmock.NewRows([]string{"vin"}).AddRow("LB9A32A22P0LS1230")) mock.ExpectQuery("SELECT vin FROM vehicle_identifier"). WithArgs("JT808", "g7s", IdentifierTypeJT808Phone, "13307795425"). WillReturnRows(sqlmock.NewRows([]string{"vin"})) mock.ExpectExec("INSERT INTO vehicle"). WithArgs("LB9A32A22P0LS1230", "粤AG18312", "G7s"). WillReturnError(errWrite) mock.ExpectRollback() report, err := ImportMappingRecords(context.Background(), db, []MappingRecord{ { SourceCode: "g7s", SourceName: "G7s", Protocol: "JT808", IdentifierType: IdentifierTypeJT808Phone, IdentifierValue: "13307795425", RawValue: "13307795425", Plate: "粤AG18312", OEM: "G7s", }, }, MappingScanReport{}, MappingImportOptions{ Apply: true, LegacyTable: "vehicle_identity_binding", }) if !errors.Is(err, errWrite) { t.Fatalf("ImportMappingRecords() error = %v, want %v", err, errWrite) } if report.Inserted != 0 || report.Resolved != 1 { t.Fatalf("report = %#v", report) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func TestNormalizeMappingPhoneHandlesExcelNumericFormats(t *testing.T) { tests := map[string]string{ "013307795425": "13307795425", "13307795425.0": "13307795425", "1.3307795425E10": "13307795425", } for input, want := range tests { if got := normalizeMappingPhone(input); got != want { t.Fatalf("normalizeMappingPhone(%q) = %q, want %q", input, got, want) } } } func writeWorkbook(t *testing.T, path string, rows [][]string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } workbook := excelize.NewFile() sheet := "Sheet1" for rowIndex, row := range rows { for columnIndex, value := range row { cellName, err := excelize.CoordinatesToCellName(columnIndex+1, rowIndex+1) if err != nil { t.Fatalf("CoordinatesToCellName() error = %v", err) } if err := workbook.SetCellValue(sheet, cellName, value); err != nil { t.Fatalf("SetCellValue() error = %v", err) } } } if err := workbook.SaveAs(path); err != nil { t.Fatalf("SaveAs() error = %v", err) } if err := workbook.Close(); err != nil { t.Fatalf("Close() error = %v", err) } }