package trafficusage import ( "fmt" "hash/fnv" "strings" "testing" "time" "unicode" ) // adminChildTableName is a verbatim copy of Admin's childTableNameWithPrefix // (iotlcm-admin-svr internal/logic/trafficusage/traffic_usage.go). It exists so // the worker rule is asserted against the production reader rule rather than // against itself: Admin's QueryDaily resolves child tables with this rule, so // any divergence here would silently hide a card's daily usage. func adminChildTableName(prefix, value string) string { var builder strings.Builder builder.WriteString(prefix) for _, r := range strings.ToLower(value) { if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' { builder.WriteRune(r) } } name := builder.String() if name == prefix || len(name) > 190 { hash := fnv.New32a() _, _ = hash.Write([]byte(value)) return fmt.Sprintf("%s%x", prefix, hash.Sum32()) } return name } func adminChildTrafficTableName(stable, iccid string) string { prefix := "sim_card_usage_m_" if stable == "t_sim_card_usage_daily" { prefix = "sim_card_usage_d_" } return adminChildTableName(prefix, iccid) } func TestChildTableNameMatchesAdminRule(t *testing.T) { longICCID := strings.Repeat("8", 191) veryLongICCID := strings.Repeat("89", 120) cases := []string{ "", "89860001123456789012", "8986-0001", "8986 0001", "8986.0001", "8986/0001", "ICCID_8986", "8986-0001-中文", "中文卡号", "____", "8986+0001", "8986'0001", longICCID, veryLongICCID, } for _, stable := range []string{"t_sim_card_usage_daily", "t_sim_card_usage_monthly"} { for _, iccid := range cases { want := adminChildTrafficTableName(stable, iccid) got := childTableName(stable, iccid) if got != want { t.Errorf("childTableName(%q, %q) = %q, admin rule yields %q", stable, iccid, got, want) } if !strings.HasPrefix(got, "sim_card_usage_") { t.Errorf("childTableName(%q, %q) = %q loses the table prefix", stable, iccid, got) } } } } // TestChildTableNameDirtyICCIDSplitsOnce guards the original defect: the old // worker rule replaced illegal characters with "_" while Admin dropped them, so // a dirty ICCID could land in a different child table than the reader queried. func TestChildTableNameDirtyICCIDSplitsOnce(t *testing.T) { for _, iccid := range []string{"8986-0001", "8986 0001", "8986.0001"} { got := childTableName("t_sim_card_usage_daily", iccid) if strings.Contains(got, "__") { t.Errorf("childTableName(%q) = %q still substitutes instead of dropping", iccid, got) } if got != "sim_card_usage_d_89860001" { t.Errorf("childTableName(%q) = %q, want the illegal characters dropped", iccid, got) } } } func TestChildTableNameLongIdentifierUsesHashFallback(t *testing.T) { iccid := strings.Repeat("8", 191) got := childTableName("t_sim_card_usage_daily", iccid) if len(got) > 190 { t.Fatalf("childTableName kept an over-long name: len=%d", len(got)) } if got != adminChildTrafficTableName("t_sim_card_usage_daily", iccid) { t.Fatalf("hash fallback diverged from the admin rule: %q", got) } } func TestSnapshotDayUsesEndOfDay(t *testing.T) { // The dashboard "yesterday top 5" and the diagnosis page both select the // 23:59:59.000 row, so the timestamp contract must not drift. day := snapshotDay(mustParse(t, "2026-10-09T13:45:00+08:00")) if got := day.Format("2006-01-02 15:04:05.000"); got != "2026-10-09 23:59:59.000" { t.Fatalf("snapshotDay = %s, want 2026-10-09 23:59:59.000", got) } } func mustParse(t *testing.T, value string) time.Time { t.Helper() parsed, err := time.Parse(time.RFC3339, value) if err != nil { t.Fatalf("parse %q: %v", value, err) } return parsed } // TestSnapshotMonthMatchesAdminRule pins the monthly timestamp contract. // // The diagnosis page reads monthly history by ts and groups it with // ts.Format("2006-01"), while admin's monthStart wrote the end-of-month // 23:59:59.000 row. A migration that changed this would leave the page reading // a month that never gets written. func TestSnapshotMonthMatchesAdminRule(t *testing.T) { // adminMonthStart is a verbatim copy of Admin's monthStart // (iotlcm-admin-svr internal/logic/trafficusage/traffic_usage.go), despite // its name returning the end of the month. adminMonthStart := func(tm time.Time) time.Time { if tm.IsZero() { tm = time.Now() } return time.Date(tm.Year(), tm.Month()+1, 0, 23, 59, 59, 0, tm.Location()) } location := time.FixedZone("UTC+8", 8*60*60) for _, month := range []time.Time{ time.Date(2026, 9, 1, 0, 0, 0, 0, location), time.Date(2026, 2, 1, 0, 0, 0, 0, location), time.Date(2026, 12, 1, 0, 0, 0, 0, location), // Leap February must land on the 29th. time.Date(2024, 2, 1, 0, 0, 0, 0, location), } { want := adminMonthStart(month).Format("2006-01-02 15:04:05.000") got := snapshotMonth(month).Format("2006-01-02 15:04:05.000") if got != want { t.Errorf("snapshotMonth(%s) = %s, admin rule yields %s", month.Format("2006-01"), got, want) } } }