sync: 同步 v1.0.4 并脱敏配置与镜像
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
package trafficusage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcfg"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/tdengine"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
)
|
||||
|
||||
type routingDriver struct{ statements []string }
|
||||
type routingConnection struct{ owner *routingDriver }
|
||||
type routingRows struct {
|
||||
count bool
|
||||
emitted bool
|
||||
}
|
||||
|
||||
func (recorder *routingDriver) Open(string) (driver.Conn, error) {
|
||||
return &routingConnection{owner: recorder}, nil
|
||||
}
|
||||
func (connection *routingConnection) Prepare(string) (driver.Stmt, error) {
|
||||
return nil, errors.New("unexpected prepare")
|
||||
}
|
||||
func (connection *routingConnection) Close() error { return nil }
|
||||
func (connection *routingConnection) Begin() (driver.Tx, error) {
|
||||
return nil, errors.New("unexpected transaction")
|
||||
}
|
||||
func (connection *routingConnection) Ping(context.Context) error { return nil }
|
||||
func (connection *routingConnection) ExecContext(_ context.Context, statement string, _ []driver.NamedValue) (driver.Result, error) {
|
||||
connection.owner.statements = append(connection.owner.statements, statement)
|
||||
return driver.RowsAffected(1), nil
|
||||
}
|
||||
func (connection *routingConnection) QueryContext(_ context.Context, statement string, _ []driver.NamedValue) (driver.Rows, error) {
|
||||
connection.owner.statements = append(connection.owner.statements, statement)
|
||||
return &routingRows{count: strings.Contains(statement, "COUNT(*)")}, nil
|
||||
}
|
||||
func (rows *routingRows) Columns() []string {
|
||||
if rows.count {
|
||||
return []string{"count"}
|
||||
}
|
||||
return []string{"ts", "usage_mb", "package_code", "iccid"}
|
||||
}
|
||||
func (rows *routingRows) Close() error { return nil }
|
||||
func (rows *routingRows) Next(values []driver.Value) error {
|
||||
if !rows.count || rows.emitted {
|
||||
return io.EOF
|
||||
}
|
||||
rows.emitted = true
|
||||
values[0] = int64(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
var routingRecorder = &routingDriver{}
|
||||
|
||||
func init() {
|
||||
sql.Register("admin_traffic_split_routing", routingRecorder)
|
||||
}
|
||||
|
||||
func TestTrafficUsageReadWriteDeleteSplitRouting(t *testing.T) {
|
||||
recorder := routingRecorder
|
||||
recorder.statements = nil
|
||||
previous := g.Cfg().GetAdapter()
|
||||
t.Cleanup(func() { _ = tdengine.Close(); g.Cfg().SetAdapter(previous) })
|
||||
adapter, err := gcfg.NewAdapterContent("tsdb:\n database: TdEngine\n tdengine:\n type: admin_traffic_split_routing\n dsn: test\n dbName: legacy\n dailyDbName: iotlcm_daily\n monthlyDbName: iotlcm_monthly\n")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g.Cfg().SetAdapter(adapter)
|
||||
_ = tdengine.Close()
|
||||
usage := NewTrafficUsage()
|
||||
ctx := context.Background()
|
||||
period := time.Date(2026, 10, 21, 0, 0, 0, 0, time.FixedZone("CST", 8*60*60))
|
||||
base := model.TrafficUsageBase{Iccid: "8986112520609988658", UsageMb: 12.5, PackageCode: "test"}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
database string
|
||||
stable string
|
||||
operation func() error
|
||||
}{
|
||||
{"write_daily", "iotlcm_daily", "sim_card_usage_d_", func() error {
|
||||
return usage.WriteDaily(ctx, []model.TrafficUsageDailyItem{{TrafficUsageBase: base, Date: period}})
|
||||
}},
|
||||
{"write_monthly", "iotlcm_monthly", "sim_card_usage_m_", func() error {
|
||||
return usage.WriteMonthly(ctx, []model.TrafficUsageMonthlyItem{{TrafficUsageBase: base, Month: period}})
|
||||
}},
|
||||
{"read_daily", "iotlcm_daily", "sim_card_usage_d_", func() error {
|
||||
_, _, err := usage.QueryDaily(ctx, model.TrafficUsageQueryInput{Iccid: base.Iccid})
|
||||
return err
|
||||
}},
|
||||
{"read_monthly", "iotlcm_monthly", "sim_card_usage_m_", func() error {
|
||||
_, _, err := usage.QueryMonthly(ctx, model.TrafficUsageQueryInput{Iccid: base.Iccid})
|
||||
return err
|
||||
}},
|
||||
{"delete_daily", "iotlcm_daily", "sim_card_usage_d_", func() error {
|
||||
return usage.DeleteDaily(ctx, model.TrafficUsageDeleteInput{Iccid: base.Iccid, StartTime: period, EndTime: period.AddDate(0, 0, 1)})
|
||||
}},
|
||||
{"delete_monthly", "iotlcm_monthly", "sim_card_usage_m_", func() error {
|
||||
return usage.DeleteMonthly(ctx, model.TrafficUsageDeleteInput{Iccid: base.Iccid, StartTime: period, EndTime: period.AddDate(0, 1, 0)})
|
||||
}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
recorder.statements = nil
|
||||
if err := test.operation(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(recorder.statements) == 0 {
|
||||
t.Fatal("no SQL issued")
|
||||
}
|
||||
for _, statement := range recorder.statements {
|
||||
normalized := strings.ReplaceAll(statement, "\x60", "")
|
||||
if !strings.Contains(normalized, test.database+"."+test.stable+base.Iccid) || strings.Contains(normalized, "legacy.") {
|
||||
t.Errorf("wrong target database: %s", statement)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,11 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/cmd/inithttp"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/tdengine"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"hash/fnv"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -97,7 +97,7 @@ func (s *sTrafficUsage) WriteDaily(ctx context.Context, items []model.TrafficUsa
|
||||
}
|
||||
|
||||
// WriteMonthly 批量写入月流量数据。
|
||||
// 月流量和日流量共用同一个 TDengine database,但使用不同超级表。
|
||||
// 月流量按配置写入月库,未配置分库时回退默认库。
|
||||
func (s *sTrafficUsage) WriteMonthly(ctx context.Context, items []model.TrafficUsageMonthlyItem) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/uuid32"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/uuid32"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
package trafficusage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
)
|
||||
|
||||
func stubDailyTrigger(t *testing.T) {
|
||||
t.Helper()
|
||||
originalNow := dailyUsageNow
|
||||
originalRunID := dailyUsageRunID
|
||||
originalEnabled := dailyUsageRabbitEnabled
|
||||
originalPublish := dailyUsagePublish
|
||||
t.Cleanup(func() {
|
||||
dailyUsageNow = originalNow
|
||||
dailyUsageRunID = originalRunID
|
||||
dailyUsageRabbitEnabled = originalEnabled
|
||||
dailyUsagePublish = originalPublish
|
||||
})
|
||||
}
|
||||
|
||||
// TestStartDailyPublishesPreviousShanghaiDay pins the period: the run must cover
|
||||
// the previous calendar day in Asia/Shanghai, matching what the legacy admin
|
||||
// scan wrote, so the migration cannot shift the day by one.
|
||||
func TestStartDailyPublishesPreviousShanghaiDay(t *testing.T) {
|
||||
stubDailyTrigger(t)
|
||||
|
||||
dailyUsageNow = func() time.Time {
|
||||
// 2026-10-10 06:00 +08:00 -> the previous day is 20261009.
|
||||
return time.Date(2026, time.October, 10, 6, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
|
||||
}
|
||||
dailyUsageRunID = func() string { return "test-run" }
|
||||
dailyUsageRabbitEnabled = func(context.Context) bool { return true }
|
||||
|
||||
var publishedQueue string
|
||||
var published model.TrafficUsageDailyStartMessage
|
||||
dailyUsagePublish = func(_ context.Context, queue string, data any) error {
|
||||
publishedQueue = queue
|
||||
published = data.(model.TrafficUsageDailyStartMessage)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := NewTrafficUsageDailySync().StartDaily(context.Background()); err != nil {
|
||||
t.Fatalf("StartDaily() error = %v", err)
|
||||
}
|
||||
if publishedQueue != consts.QueueTrafficUsageDailyStart {
|
||||
t.Fatalf("queue = %q, want %q", publishedQueue, consts.QueueTrafficUsageDailyStart)
|
||||
}
|
||||
if published.QueryDate != "20261009" {
|
||||
t.Fatalf("queryDate = %q, want 20261009", published.QueryDate)
|
||||
}
|
||||
if published.RunID != "daily-usage-20261009-test-run" {
|
||||
t.Fatalf("runId = %q", published.RunID)
|
||||
}
|
||||
if published.Operation != model.OperationPreviousDayUsageFullSync {
|
||||
t.Fatalf("operation = %q", published.Operation)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStartDailyUsesShanghaiDayAcrossUTCMidnight guards the timezone bug that
|
||||
// the legacy code carried: a UTC instant just after midnight is still the
|
||||
// previous day in Shanghai, so the period must not be computed in UTC.
|
||||
func TestStartDailyUsesShanghaiDayAcrossUTCMidnight(t *testing.T) {
|
||||
stubDailyTrigger(t)
|
||||
|
||||
// 2026-10-09 17:30 UTC == 2026-10-10 01:30 +08:00 -> previous day 20261009.
|
||||
dailyUsageNow = func() time.Time {
|
||||
return time.Date(2026, time.October, 9, 17, 30, 0, 0, time.UTC)
|
||||
}
|
||||
dailyUsageRunID = func() string { return "tz-run" }
|
||||
dailyUsageRabbitEnabled = func(context.Context) bool { return true }
|
||||
|
||||
var published model.TrafficUsageDailyStartMessage
|
||||
dailyUsagePublish = func(_ context.Context, _ string, data any) error {
|
||||
published = data.(model.TrafficUsageDailyStartMessage)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := NewTrafficUsageDailySync().StartDaily(context.Background()); err != nil {
|
||||
t.Fatalf("StartDaily() error = %v", err)
|
||||
}
|
||||
if published.QueryDate != "20261009" {
|
||||
t.Fatalf("queryDate = %q, want 20261009 (Shanghai, not UTC)", published.QueryDate)
|
||||
}
|
||||
if published.TriggeredAt != "2026-10-10T01:30:00+08:00" {
|
||||
t.Fatalf("triggeredAt = %q, want the Shanghai instant", published.TriggeredAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartDailyRunScopedForwardsScope(t *testing.T) {
|
||||
stubDailyTrigger(t)
|
||||
|
||||
dailyUsageNow = func() time.Time {
|
||||
return time.Date(2026, time.October, 10, 6, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
|
||||
}
|
||||
dailyUsageRunID = func() string { return "scoped-run" }
|
||||
dailyUsageRabbitEnabled = func(context.Context) bool { return true }
|
||||
|
||||
var published model.TrafficUsageDailyStartMessage
|
||||
dailyUsagePublish = func(_ context.Context, _ string, data any) error {
|
||||
published = data.(model.TrafficUsageDailyStartMessage)
|
||||
return nil
|
||||
}
|
||||
|
||||
runID, err := NewTrafficUsageDailySync().StartDailyRunScoped(context.Background(), &model.SyncScope{Channels: []string{"217"}})
|
||||
if err != nil {
|
||||
t.Fatalf("StartDailyRunScoped() error = %v", err)
|
||||
}
|
||||
if runID != "daily-usage-20261009-scoped-run" {
|
||||
t.Fatalf("runId = %q", runID)
|
||||
}
|
||||
if published.Scope == nil || published.Scope.ChannelCount() != 1 || !published.Scope.Allows("217") {
|
||||
t.Fatalf("scope = %+v, want channel 217 only", published.Scope)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartDailyRejectsInvalidScope(t *testing.T) {
|
||||
stubDailyTrigger(t)
|
||||
|
||||
dailyUsageRabbitEnabled = func(context.Context) bool { return true }
|
||||
called := false
|
||||
dailyUsagePublish = func(context.Context, string, any) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := NewTrafficUsageDailySync().StartDailyRunScoped(context.Background(), &model.SyncScope{Channels: []string{"invalid/chan"}}); err == nil {
|
||||
t.Fatal("expected an invalid chanKey to be rejected")
|
||||
}
|
||||
if called {
|
||||
t.Fatal("publisher called for a rejected scope")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartDailyReturnsPublishError(t *testing.T) {
|
||||
stubDailyTrigger(t)
|
||||
|
||||
dailyUsageNow = func() time.Time { return time.Date(2026, time.October, 10, 6, 0, 0, 0, time.UTC) }
|
||||
dailyUsageRunID = func() string { return "err-run" }
|
||||
dailyUsageRabbitEnabled = func(context.Context) bool { return true }
|
||||
dailyUsagePublish = func(context.Context, string, any) error {
|
||||
return errors.New("broker unavailable")
|
||||
}
|
||||
|
||||
if err := NewTrafficUsageDailySync().StartDaily(context.Background()); err == nil {
|
||||
t.Fatal("StartDaily() error = nil, want publish error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartDailySkipsWhenRabbitMQDisabled(t *testing.T) {
|
||||
stubDailyTrigger(t)
|
||||
|
||||
dailyUsageRabbitEnabled = func(context.Context) bool { return false }
|
||||
called := false
|
||||
dailyUsagePublish = func(context.Context, string, any) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := NewTrafficUsageDailySync().StartDaily(context.Background()); err == nil {
|
||||
t.Fatal("StartDaily() error = nil, want disabled error")
|
||||
}
|
||||
if called {
|
||||
t.Fatal("publisher called while RabbitMQ is disabled")
|
||||
}
|
||||
}
|
||||
|
||||
// dailyBackfillClock pins "now" for the period-aware daily tests: 2026-10-20
|
||||
// 06:00 +08:00, so the default day is 20261019 and nothing later is valid.
|
||||
func dailyBackfillClock(t *testing.T) {
|
||||
t.Helper()
|
||||
stubDailyTrigger(t)
|
||||
dailyUsageNow = func() time.Time {
|
||||
return time.Date(2026, time.October, 20, 6, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
|
||||
}
|
||||
dailyUsageRunID = func() string { return "backfill-run" }
|
||||
dailyUsageRabbitEnabled = func(context.Context) bool { return true }
|
||||
}
|
||||
|
||||
func captureDailyStart(t *testing.T) *model.TrafficUsageDailyStartMessage {
|
||||
t.Helper()
|
||||
published := &model.TrafficUsageDailyStartMessage{}
|
||||
dailyUsagePublish = func(_ context.Context, _ string, data any) error {
|
||||
*published = data.(model.TrafficUsageDailyStartMessage)
|
||||
return nil
|
||||
}
|
||||
return published
|
||||
}
|
||||
|
||||
func TestStartDailyRunScopedAtUsesGivenDate(t *testing.T) {
|
||||
dailyBackfillClock(t)
|
||||
published := captureDailyStart(t)
|
||||
|
||||
runID, err := NewTrafficUsageDailySync().StartDailyRunScopedAt(context.Background(), nil, "20261014")
|
||||
if err != nil {
|
||||
t.Fatalf("StartDailyRunScopedAt() error = %v", err)
|
||||
}
|
||||
if published.QueryDate != "20261014" {
|
||||
t.Fatalf("queryDate = %q, want 20261014", published.QueryDate)
|
||||
}
|
||||
if runID != "daily-usage-20261014-backfill-run" {
|
||||
t.Fatalf("runId = %q", runID)
|
||||
}
|
||||
if published.Operation != model.OperationPreviousDayUsageFullSync {
|
||||
t.Fatalf("operation = %q", published.Operation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartDailyRunScopedAtRejectsFutureDate(t *testing.T) {
|
||||
dailyBackfillClock(t)
|
||||
called := false
|
||||
dailyUsagePublish = func(context.Context, string, any) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, queryDate := range []string{"20261020", "20261021", "20270101"} {
|
||||
if _, err := NewTrafficUsageDailySync().StartDailyRunScopedAt(context.Background(), nil, queryDate); err == nil {
|
||||
t.Fatalf("queryDate %q accepted, want a future-date error", queryDate)
|
||||
}
|
||||
}
|
||||
if called {
|
||||
t.Fatal("publisher called for a rejected date")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartDailyRunScopedAtRejectsMalformedDate(t *testing.T) {
|
||||
dailyBackfillClock(t)
|
||||
called := false
|
||||
dailyUsagePublish = func(context.Context, string, any) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, queryDate := range []string{"2026-10-14", "abc", "2026101", "202610140", "20261314"} {
|
||||
if _, err := NewTrafficUsageDailySync().StartDailyRunScopedAt(context.Background(), nil, queryDate); err == nil {
|
||||
t.Fatalf("queryDate %q accepted, want a format error", queryDate)
|
||||
}
|
||||
}
|
||||
if called {
|
||||
t.Fatal("publisher called for a malformed date")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartDailyRunScopedAtRejectsDateBeforeLowerBound(t *testing.T) {
|
||||
dailyBackfillClock(t)
|
||||
called := false
|
||||
dailyUsagePublish = func(context.Context, string, any) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, queryDate := range []string{"20191231", "19990101"} {
|
||||
if _, err := NewTrafficUsageDailySync().StartDailyRunScopedAt(context.Background(), nil, queryDate); err == nil {
|
||||
t.Fatalf("queryDate %q accepted, want a lower-bound error", queryDate)
|
||||
}
|
||||
}
|
||||
if called {
|
||||
t.Fatal("publisher called for a date below the lower bound")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartDailyRunScopedAtEmptyFallsBackToYesterday(t *testing.T) {
|
||||
dailyBackfillClock(t)
|
||||
published := captureDailyStart(t)
|
||||
|
||||
runID, err := NewTrafficUsageDailySync().StartDailyRunScopedAt(context.Background(), nil, "")
|
||||
if err != nil {
|
||||
t.Fatalf("StartDailyRunScopedAt() error = %v", err)
|
||||
}
|
||||
if published.QueryDate != "20261019" {
|
||||
t.Fatalf("queryDate = %q, want 20261019 (yesterday)", published.QueryDate)
|
||||
}
|
||||
if runID != "daily-usage-20261019-backfill-run" {
|
||||
t.Fatalf("runId = %q", runID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStartDailyRunScopedAtEmptyMatchesLegacyScoped keeps the backward
|
||||
// compatibility promise concrete: the old entry point and an empty period must
|
||||
// publish byte-identical messages.
|
||||
func TestStartDailyRunScopedAtEmptyMatchesLegacyScoped(t *testing.T) {
|
||||
dailyBackfillClock(t)
|
||||
var messages []model.TrafficUsageDailyStartMessage
|
||||
dailyUsagePublish = func(_ context.Context, _ string, data any) error {
|
||||
messages = append(messages, data.(model.TrafficUsageDailyStartMessage))
|
||||
return nil
|
||||
}
|
||||
|
||||
legacyID, err := NewTrafficUsageDailySync().StartDailyRunScoped(context.Background(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
atID, err := NewTrafficUsageDailySync().StartDailyRunScopedAt(context.Background(), nil, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(messages) != 2 {
|
||||
t.Fatalf("published %d messages, want 2", len(messages))
|
||||
}
|
||||
if legacyID != atID || messages[0] != messages[1] {
|
||||
t.Fatalf("legacy = %+v/%q, at = %+v/%q", messages[0], legacyID, messages[1], atID)
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/uuid32"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/uuid32"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
package trafficusage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
)
|
||||
|
||||
func stubMonthlyHistoryTrigger(t *testing.T) {
|
||||
t.Helper()
|
||||
originalNow := monthlyHistoryNow
|
||||
originalRunID := monthlyHistoryRunID
|
||||
originalEnabled := monthlyHistoryRabbitEnabled
|
||||
originalPublish := monthlyHistoryPublish
|
||||
t.Cleanup(func() {
|
||||
monthlyHistoryNow = originalNow
|
||||
monthlyHistoryRunID = originalRunID
|
||||
monthlyHistoryRabbitEnabled = originalEnabled
|
||||
monthlyHistoryPublish = originalPublish
|
||||
})
|
||||
}
|
||||
|
||||
// TestStartMonthlyHistoryPublishesPreviousMonth pins the period: the run must
|
||||
// backfill the previous calendar month in Asia/Shanghai, matching what the
|
||||
// legacy SyncMonthly scan wrote.
|
||||
func TestStartMonthlyHistoryPublishesPreviousMonth(t *testing.T) {
|
||||
stubMonthlyHistoryTrigger(t)
|
||||
|
||||
monthlyHistoryNow = func() time.Time {
|
||||
return time.Date(2026, time.October, 10, 6, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
|
||||
}
|
||||
monthlyHistoryRunID = func() string { return "test-run" }
|
||||
monthlyHistoryRabbitEnabled = func(context.Context) bool { return true }
|
||||
|
||||
var publishedQueue string
|
||||
var published model.TrafficUsageMonthlyHistoryStartMessage
|
||||
monthlyHistoryPublish = func(_ context.Context, queue string, data any) error {
|
||||
publishedQueue = queue
|
||||
published = data.(model.TrafficUsageMonthlyHistoryStartMessage)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := NewTrafficUsageMonthlySync().StartMonthlyHistory(context.Background()); err != nil {
|
||||
t.Fatalf("StartMonthlyHistory() error = %v", err)
|
||||
}
|
||||
if publishedQueue != consts.QueueTrafficUsageMonthlyHistoryStart {
|
||||
t.Fatalf("queue = %q, want %q", publishedQueue, consts.QueueTrafficUsageMonthlyHistoryStart)
|
||||
}
|
||||
if published.QueryMonth != "202609" {
|
||||
t.Fatalf("queryMonth = %q, want 202609", published.QueryMonth)
|
||||
}
|
||||
if published.RunID != "monthly-history-202609-test-run" {
|
||||
t.Fatalf("runId = %q", published.RunID)
|
||||
}
|
||||
if published.Operation != model.OperationPreviousMonthUsageFullSync {
|
||||
t.Fatalf("operation = %q", published.Operation)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStartMonthlyHistoryHandlesJanuaryRollover guards the year boundary: the
|
||||
// previous month of January is December of the prior year.
|
||||
func TestStartMonthlyHistoryHandlesJanuaryRollover(t *testing.T) {
|
||||
stubMonthlyHistoryTrigger(t)
|
||||
|
||||
monthlyHistoryNow = func() time.Time {
|
||||
return time.Date(2026, time.January, 5, 6, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
|
||||
}
|
||||
monthlyHistoryRunID = func() string { return "jan-run" }
|
||||
monthlyHistoryRabbitEnabled = func(context.Context) bool { return true }
|
||||
|
||||
var published model.TrafficUsageMonthlyHistoryStartMessage
|
||||
monthlyHistoryPublish = func(_ context.Context, _ string, data any) error {
|
||||
published = data.(model.TrafficUsageMonthlyHistoryStartMessage)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := NewTrafficUsageMonthlySync().StartMonthlyHistory(context.Background()); err != nil {
|
||||
t.Fatalf("StartMonthlyHistory() error = %v", err)
|
||||
}
|
||||
if published.QueryMonth != "202512" {
|
||||
t.Fatalf("queryMonth = %q, want 202512 (January rolls back to the previous December)", published.QueryMonth)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStartMonthlyHistoryUsesShanghaiMonthAcrossUTCMidnight guards the timezone
|
||||
// bug the legacy code carried: a UTC instant on the 1st is still the last day of
|
||||
// the previous month in Shanghai.
|
||||
func TestStartMonthlyHistoryUsesShanghaiMonthAcrossUTCMidnight(t *testing.T) {
|
||||
stubMonthlyHistoryTrigger(t)
|
||||
|
||||
// 2026-09-30 17:30 UTC == 2026-10-01 01:30 +08:00 -> previous month 202609.
|
||||
monthlyHistoryNow = func() time.Time {
|
||||
return time.Date(2026, time.September, 30, 17, 30, 0, 0, time.UTC)
|
||||
}
|
||||
monthlyHistoryRunID = func() string { return "tz-run" }
|
||||
monthlyHistoryRabbitEnabled = func(context.Context) bool { return true }
|
||||
|
||||
var published model.TrafficUsageMonthlyHistoryStartMessage
|
||||
monthlyHistoryPublish = func(_ context.Context, _ string, data any) error {
|
||||
published = data.(model.TrafficUsageMonthlyHistoryStartMessage)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := NewTrafficUsageMonthlySync().StartMonthlyHistory(context.Background()); err != nil {
|
||||
t.Fatalf("StartMonthlyHistory() error = %v", err)
|
||||
}
|
||||
if published.QueryMonth != "202609" {
|
||||
t.Fatalf("queryMonth = %q, want 202609 (Shanghai, not UTC)", published.QueryMonth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartMonthlyHistoryRunScopedForwardsScope(t *testing.T) {
|
||||
stubMonthlyHistoryTrigger(t)
|
||||
|
||||
monthlyHistoryNow = func() time.Time {
|
||||
return time.Date(2026, time.October, 10, 6, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
|
||||
}
|
||||
monthlyHistoryRunID = func() string { return "scoped-run" }
|
||||
monthlyHistoryRabbitEnabled = func(context.Context) bool { return true }
|
||||
|
||||
var published model.TrafficUsageMonthlyHistoryStartMessage
|
||||
monthlyHistoryPublish = func(_ context.Context, _ string, data any) error {
|
||||
published = data.(model.TrafficUsageMonthlyHistoryStartMessage)
|
||||
return nil
|
||||
}
|
||||
|
||||
runID, err := NewTrafficUsageMonthlySync().StartMonthlyHistoryRunScoped(context.Background(), &model.SyncScope{Channels: []string{"217"}})
|
||||
if err != nil {
|
||||
t.Fatalf("StartMonthlyHistoryRunScoped() error = %v", err)
|
||||
}
|
||||
if runID != "monthly-history-202609-scoped-run" {
|
||||
t.Fatalf("runId = %q", runID)
|
||||
}
|
||||
if published.Scope == nil || published.Scope.ChannelCount() != 1 || !published.Scope.Allows("217") {
|
||||
t.Fatalf("scope = %+v, want channel 217 only", published.Scope)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartMonthlyHistoryRejectsInvalidScope(t *testing.T) {
|
||||
stubMonthlyHistoryTrigger(t)
|
||||
|
||||
monthlyHistoryRabbitEnabled = func(context.Context) bool { return true }
|
||||
called := false
|
||||
monthlyHistoryPublish = func(context.Context, string, any) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := NewTrafficUsageMonthlySync().StartMonthlyHistoryRunScoped(context.Background(), &model.SyncScope{Channels: []string{"invalid/chan"}}); err == nil {
|
||||
t.Fatal("expected an invalid chanKey to be rejected")
|
||||
}
|
||||
if called {
|
||||
t.Fatal("publisher called for a rejected scope")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartMonthlyHistoryReturnsPublishError(t *testing.T) {
|
||||
stubMonthlyHistoryTrigger(t)
|
||||
|
||||
monthlyHistoryNow = func() time.Time { return time.Date(2026, time.October, 10, 6, 0, 0, 0, time.UTC) }
|
||||
monthlyHistoryRunID = func() string { return "err-run" }
|
||||
monthlyHistoryRabbitEnabled = func(context.Context) bool { return true }
|
||||
monthlyHistoryPublish = func(context.Context, string, any) error {
|
||||
return errors.New("broker unavailable")
|
||||
}
|
||||
|
||||
if err := NewTrafficUsageMonthlySync().StartMonthlyHistory(context.Background()); err == nil {
|
||||
t.Fatal("StartMonthlyHistory() error = nil, want publish error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartMonthlyHistorySkipsWhenRabbitMQDisabled(t *testing.T) {
|
||||
stubMonthlyHistoryTrigger(t)
|
||||
|
||||
monthlyHistoryRabbitEnabled = func(context.Context) bool { return false }
|
||||
called := false
|
||||
monthlyHistoryPublish = func(context.Context, string, any) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := NewTrafficUsageMonthlySync().StartMonthlyHistory(context.Background()); err == nil {
|
||||
t.Fatal("StartMonthlyHistory() error = nil, want disabled error")
|
||||
}
|
||||
if called {
|
||||
t.Fatal("publisher called while RabbitMQ is disabled")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMonthlyHistoryAndCurrentMonthTriggersUseDistinctQueues keeps the two
|
||||
// monthly chains from crossing: one fills the MySQL snapshot, the other the
|
||||
// TDengine monthly table.
|
||||
func TestMonthlyHistoryAndCurrentMonthTriggersUseDistinctQueues(t *testing.T) {
|
||||
stubMonthlyHistoryTrigger(t)
|
||||
originalNow := monthlyUsageNow
|
||||
originalRunID := monthlyUsageRunID
|
||||
originalEnabled := monthlyUsageRabbitEnabled
|
||||
originalPublish := monthlyUsagePublish
|
||||
t.Cleanup(func() {
|
||||
monthlyUsageNow = originalNow
|
||||
monthlyUsageRunID = originalRunID
|
||||
monthlyUsageRabbitEnabled = originalEnabled
|
||||
monthlyUsagePublish = originalPublish
|
||||
})
|
||||
|
||||
fixed := time.Date(2026, time.October, 10, 6, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
|
||||
monthlyHistoryNow = func() time.Time { return fixed }
|
||||
monthlyHistoryRunID = func() string { return "r" }
|
||||
monthlyHistoryRabbitEnabled = func(context.Context) bool { return true }
|
||||
monthlyUsageNow = func() time.Time { return fixed }
|
||||
monthlyUsageRunID = func() string { return "r" }
|
||||
monthlyUsageRabbitEnabled = func(context.Context) bool { return true }
|
||||
|
||||
var historyQueue, currentQueue string
|
||||
monthlyHistoryPublish = func(_ context.Context, queue string, _ any) error {
|
||||
historyQueue = queue
|
||||
return nil
|
||||
}
|
||||
monthlyUsagePublish = func(_ context.Context, queue string, _ any) error {
|
||||
currentQueue = queue
|
||||
return nil
|
||||
}
|
||||
|
||||
sync := NewTrafficUsageMonthlySync()
|
||||
if err := sync.StartMonthlyHistory(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := sync.StartMonthly(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if historyQueue == currentQueue {
|
||||
t.Fatalf("both monthly chains publish to %q; they must stay separate", historyQueue)
|
||||
}
|
||||
}
|
||||
|
||||
// monthlyHistoryBackfillClock pins "now" for the period-aware monthly tests:
|
||||
// 2026-10-20 06:00 +08:00, so the current month is 202610, the default month is
|
||||
// 202609, and nothing from 202610 onwards is a valid history target.
|
||||
func monthlyHistoryBackfillClock(t *testing.T) {
|
||||
t.Helper()
|
||||
stubMonthlyHistoryTrigger(t)
|
||||
monthlyHistoryNow = func() time.Time {
|
||||
return time.Date(2026, time.October, 20, 6, 0, 0, 0, time.FixedZone("UTC+8", 8*60*60))
|
||||
}
|
||||
monthlyHistoryRunID = func() string { return "backfill-run" }
|
||||
monthlyHistoryRabbitEnabled = func(context.Context) bool { return true }
|
||||
}
|
||||
|
||||
func captureMonthlyHistoryStart(t *testing.T) *model.TrafficUsageMonthlyHistoryStartMessage {
|
||||
t.Helper()
|
||||
published := &model.TrafficUsageMonthlyHistoryStartMessage{}
|
||||
monthlyHistoryPublish = func(_ context.Context, _ string, data any) error {
|
||||
*published = data.(model.TrafficUsageMonthlyHistoryStartMessage)
|
||||
return nil
|
||||
}
|
||||
return published
|
||||
}
|
||||
|
||||
func TestStartMonthlyHistoryRunScopedAtUsesGivenMonth(t *testing.T) {
|
||||
monthlyHistoryBackfillClock(t)
|
||||
published := captureMonthlyHistoryStart(t)
|
||||
|
||||
runID, err := NewTrafficUsageMonthlySync().StartMonthlyHistoryRunScopedAt(context.Background(), nil, "202604")
|
||||
if err != nil {
|
||||
t.Fatalf("StartMonthlyHistoryRunScopedAt() error = %v", err)
|
||||
}
|
||||
if published.QueryMonth != "202604" {
|
||||
t.Fatalf("queryMonth = %q, want 202604", published.QueryMonth)
|
||||
}
|
||||
if runID != "monthly-history-202604-backfill-run" {
|
||||
t.Fatalf("runId = %q", runID)
|
||||
}
|
||||
if published.Operation != model.OperationPreviousMonthUsageFullSync {
|
||||
t.Fatalf("operation = %q", published.Operation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartMonthlyHistoryRunScopedAtRejectsCurrentMonth(t *testing.T) {
|
||||
monthlyHistoryBackfillClock(t)
|
||||
called := false
|
||||
monthlyHistoryPublish = func(context.Context, string, any) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// The current month belongs to the MySQL snapshot chain, not this one.
|
||||
if _, err := NewTrafficUsageMonthlySync().StartMonthlyHistoryRunScopedAt(context.Background(), nil, "202610"); err == nil {
|
||||
t.Fatal("current month accepted, want an error")
|
||||
}
|
||||
if called {
|
||||
t.Fatal("publisher called for the current month")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartMonthlyHistoryRunScopedAtRejectsFutureMonth(t *testing.T) {
|
||||
monthlyHistoryBackfillClock(t)
|
||||
called := false
|
||||
monthlyHistoryPublish = func(context.Context, string, any) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, queryMonth := range []string{"202612", "202701"} {
|
||||
if _, err := NewTrafficUsageMonthlySync().StartMonthlyHistoryRunScopedAt(context.Background(), nil, queryMonth); err == nil {
|
||||
t.Fatalf("queryMonth %q accepted, want a future-month error", queryMonth)
|
||||
}
|
||||
}
|
||||
if called {
|
||||
t.Fatal("publisher called for a rejected month")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartMonthlyHistoryRunScopedAtRejectsMalformed(t *testing.T) {
|
||||
monthlyHistoryBackfillClock(t)
|
||||
called := false
|
||||
monthlyHistoryPublish = func(context.Context, string, any) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, queryMonth := range []string{"2026-04", "20264", "abc", "20260401", "202613"} {
|
||||
if _, err := NewTrafficUsageMonthlySync().StartMonthlyHistoryRunScopedAt(context.Background(), nil, queryMonth); err == nil {
|
||||
t.Fatalf("queryMonth %q accepted, want a format error", queryMonth)
|
||||
}
|
||||
}
|
||||
if called {
|
||||
t.Fatal("publisher called for a malformed month")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartMonthlyHistoryRunScopedAtRejectsMonthBeforeLowerBound(t *testing.T) {
|
||||
monthlyHistoryBackfillClock(t)
|
||||
called := false
|
||||
monthlyHistoryPublish = func(context.Context, string, any) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := NewTrafficUsageMonthlySync().StartMonthlyHistoryRunScopedAt(context.Background(), nil, "201912"); err == nil {
|
||||
t.Fatal("month below the lower bound accepted, want an error")
|
||||
}
|
||||
if called {
|
||||
t.Fatal("publisher called for a month below the lower bound")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartMonthlyHistoryRunScopedAtEmptyFallsBackToPreviousMonth(t *testing.T) {
|
||||
monthlyHistoryBackfillClock(t)
|
||||
published := captureMonthlyHistoryStart(t)
|
||||
|
||||
runID, err := NewTrafficUsageMonthlySync().StartMonthlyHistoryRunScopedAt(context.Background(), nil, "")
|
||||
if err != nil {
|
||||
t.Fatalf("StartMonthlyHistoryRunScopedAt() error = %v", err)
|
||||
}
|
||||
if published.QueryMonth != "202609" {
|
||||
t.Fatalf("queryMonth = %q, want 202609 (previous month)", published.QueryMonth)
|
||||
}
|
||||
if runID != "monthly-history-202609-backfill-run" {
|
||||
t.Fatalf("runId = %q", runID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStartMonthlyHistoryRunScopedAtEmptyMatchesLegacyScoped keeps the backward
|
||||
// compatibility promise concrete: the old entry point and an empty period must
|
||||
// publish identical messages.
|
||||
func TestStartMonthlyHistoryRunScopedAtEmptyMatchesLegacyScoped(t *testing.T) {
|
||||
monthlyHistoryBackfillClock(t)
|
||||
var messages []model.TrafficUsageMonthlyHistoryStartMessage
|
||||
monthlyHistoryPublish = func(_ context.Context, _ string, data any) error {
|
||||
messages = append(messages, data.(model.TrafficUsageMonthlyHistoryStartMessage))
|
||||
return nil
|
||||
}
|
||||
|
||||
legacyID, err := NewTrafficUsageMonthlySync().StartMonthlyHistoryRunScoped(context.Background(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
atID, err := NewTrafficUsageMonthlySync().StartMonthlyHistoryRunScopedAt(context.Background(), nil, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(messages) != 2 {
|
||||
t.Fatalf("published %d messages, want 2", len(messages))
|
||||
}
|
||||
if legacyID != atID || messages[0] != messages[1] {
|
||||
t.Fatalf("legacy = %+v/%q, at = %+v/%q", messages[0], legacyID, messages[1], atID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package trafficusage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
)
|
||||
|
||||
func TestStartMonthlyPublishesShanghaiMonth(t *testing.T) {
|
||||
originalNow := monthlyUsageNow
|
||||
originalRunID := monthlyUsageRunID
|
||||
originalEnabled := monthlyUsageRabbitEnabled
|
||||
originalPublish := monthlyUsagePublish
|
||||
t.Cleanup(func() {
|
||||
monthlyUsageNow = originalNow
|
||||
monthlyUsageRunID = originalRunID
|
||||
monthlyUsageRabbitEnabled = originalEnabled
|
||||
monthlyUsagePublish = originalPublish
|
||||
})
|
||||
|
||||
monthlyUsageNow = func() time.Time {
|
||||
return time.Date(2026, time.August, 31, 23, 30, 0, 0, time.UTC)
|
||||
}
|
||||
monthlyUsageRunID = func() string { return "test-run" }
|
||||
monthlyUsageRabbitEnabled = func(context.Context) bool { return true }
|
||||
|
||||
var publishedQueue string
|
||||
var published model.TrafficUsageMonthlyStartMessage
|
||||
monthlyUsagePublish = func(_ context.Context, queue string, data any) error {
|
||||
publishedQueue = queue
|
||||
published = data.(model.TrafficUsageMonthlyStartMessage)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := NewTrafficUsageMonthlySync().StartMonthly(context.Background()); err != nil {
|
||||
t.Fatalf("StartMonthly() error = %v", err)
|
||||
}
|
||||
if publishedQueue != consts.QueueTrafficUsageMonthlyStart {
|
||||
t.Fatalf("queue = %q, want %q", publishedQueue, consts.QueueTrafficUsageMonthlyStart)
|
||||
}
|
||||
if published.RunID != "current-month-usage-20260901-test-run" {
|
||||
t.Fatalf("runId = %q", published.RunID)
|
||||
}
|
||||
if published.Operation != monthlyUsageFullSyncOperation {
|
||||
t.Fatalf("operation = %q", published.Operation)
|
||||
}
|
||||
if published.QueryMonth != "202609" {
|
||||
t.Fatalf("queryMonth = %q, want 202609", published.QueryMonth)
|
||||
}
|
||||
if published.TriggeredAt != "2026-09-01T07:30:00+08:00" {
|
||||
t.Fatalf("triggeredAt = %q", published.TriggeredAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartMonthlyReturnsPublishError(t *testing.T) {
|
||||
originalEnabled := monthlyUsageRabbitEnabled
|
||||
originalPublish := monthlyUsagePublish
|
||||
t.Cleanup(func() {
|
||||
monthlyUsageRabbitEnabled = originalEnabled
|
||||
monthlyUsagePublish = originalPublish
|
||||
})
|
||||
|
||||
monthlyUsageRabbitEnabled = func(context.Context) bool { return true }
|
||||
monthlyUsagePublish = func(context.Context, string, any) error {
|
||||
return errors.New("broker unavailable")
|
||||
}
|
||||
|
||||
if err := NewTrafficUsageMonthlySync().StartMonthly(context.Background()); err == nil {
|
||||
t.Fatal("StartMonthly() error = nil, want publish error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartMonthlySkipsWhenRabbitMQDisabled(t *testing.T) {
|
||||
originalEnabled := monthlyUsageRabbitEnabled
|
||||
originalPublish := monthlyUsagePublish
|
||||
t.Cleanup(func() {
|
||||
monthlyUsageRabbitEnabled = originalEnabled
|
||||
monthlyUsagePublish = originalPublish
|
||||
})
|
||||
|
||||
monthlyUsageRabbitEnabled = func(context.Context) bool { return false }
|
||||
called := false
|
||||
monthlyUsagePublish = func(context.Context, string, any) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := NewTrafficUsageMonthlySync().StartMonthly(context.Background()); err == nil {
|
||||
t.Fatal("StartMonthly() error = nil, want disabled error")
|
||||
}
|
||||
if called {
|
||||
t.Fatal("publisher called while RabbitMQ is disabled")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user