Files
iotlcm/worker-svr/internal/producer/monthlyusagehistory/channel_scanner_test.go
T

124 lines
5.0 KiB
Go

package monthlyusagehistory
import (
"context"
"reflect"
"sort"
"strings"
"testing"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/dao"
)
// SQLite runs the SAME predicate and ordering with only the two MySQL hints
// removed. This checks full eligibility/TaskID parity, not MySQL access plans.
// Production EXPLAIN must still validate the unmodified channelPageSQL.
type sqliteChannelScanner struct{ DatabaseScanner }
func (s *sqliteChannelScanner) ScanChannel(ctx context.Context, key string, last int64, size int) ([]Card, error) {
query := strings.ReplaceAll(channelPageSQL, "FORCE INDEX (idx_card_chan_key)", "")
query = strings.ReplaceAll(query, "STRAIGHT_JOIN", "INNER JOIN")
var rows []struct {
ID int64 `orm:"id"`
ICCID string `orm:"iccid"`
MSISDN string `orm:"msisdn"`
IMSI string `orm:"imsi"`
ChanKey string `orm:"chan_key"`
PackageCode string `orm:"current_package_code"`
}
if err := dao.TSimCardInfo.DB().Raw(query, key, last, size).Ctx(ctx).Scan(&rows); err != nil {
return nil, err
}
cards := make([]Card, 0, len(rows))
for _, row := range rows {
cards = append(cards, Card{ID: row.ID, ICCID: row.ICCID, MSISDN: row.MSISDN, IMSI: row.IMSI, ChanKey: row.ChanKey, PackageCode: row.PackageCode})
}
return cards, nil
}
func TestDatabaseQualificationAndCompleteTaskIDParity(t *testing.T) {
// No application DB/config: isolated SQLite file and loopback Redis only.
if err := gdb.SetConfig(gdb.Config{"default": gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(" + t.TempDir() + "/cards.db)"}}}); err != nil {
t.Fatal(err)
}
ctx := contextForTest(t)
db := dao.TSimCardInfo.DB()
t.Cleanup(func() { _ = db.Close(context.Background()) })
for _, sql := range []string{
`CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, tenant_id TEXT, deleted_at DATETIME)`,
`CREATE TABLE t_sim_card_info (id INTEGER PRIMARY KEY, iccid TEXT, msisdn TEXT, imsi TEXT, chan_key TEXT, current_package_code TEXT, dept_id INTEGER, tenant_id TEXT)`,
`CREATE INDEX idx_card_chan_key ON t_sim_card_info(chan_key)`,
`CREATE TABLE t_sim_channels (chan_key TEXT, temp_key TEXT, sync_enabled INTEGER, operator INTEGER)`,
`INSERT INTO sys_dept VALUES (1,'t1',NULL),(2,'t2',NULL),(3,'t1','2026-01-01')`,
`INSERT INTO t_sim_channels VALUES ('a','sim_yd_v5',0,1),('b','sim_dx_v1',0,2),('disabled','sim_yd_v5',1,1),('no-template','',0,1)`,
`INSERT INTO t_sim_card_info VALUES
(1,'i1','m1','s1','a','pkg',1,'t1'),
(2,'i2','m2','s2','b','pkg',2,'t2'),
(3,'i3','','','a','pkg',1,'t1'),
(4,'','','','a','pkg',1,'t1'),
(5,'i5','','','a','',1,'t1'),
(6,'i6','','','a','pkg',1,'wrong-tenant'),
(7,'i7','','','a','pkg',3,'t1'),
(8,'i8','','','b','pkg',99,'t2'),
(9,'i9','','','disabled','pkg',1,'t1'),
(10,'i10','','','no-template','pkg',1,'t1'),
(11,'i11','','','unknown','pkg',1,'t1'),
(12,'i12','','','','pkg',1,'t1'),
(13,'i13','','','b','pkg',2,'t2')`,
} {
if _, err := db.Exec(ctx, sql); err != nil {
t.Fatal(err)
}
}
store := testStore(t)
for _, scope := range [][]string{nil, {"a"}, {"b", "disabled", "no-template", "unknown"}, {"unknown"}} {
var sets [][]string
for _, mode := range []string{"serial", "channel"} {
scanner := &sqliteChannelScanner{}
p := testProducer(t, scanner, store, Config{ScanMode: mode, PageSize: 2})
run := "db-" + mode
if err := p.Produce(ctx, scopedMessage(run, scope...)); err != nil {
t.Fatal(err)
}
ids := taskIDs(t, store, run)
for i, id := range ids {
ids[i] = strings.TrimPrefix(id, run+":")
}
sort.Strings(ids)
sets = append(sets, ids)
stateKey, _ := store.TaskStateKey(run)
metaKey, _ := store.RunMetaKey(run)
chanKey, _ := store.RunChannelsKey(run)
_, _ = store.RedisClient().Do(ctx, "DEL", stateKey, metaKey, chanKey)
for _, key := range []string{"a", "b"} {
activeKey, _ := store.RoundActiveKey(run, 1, key)
_, _ = store.RedisClient().Do(ctx, "DEL", activeKey)
}
_, _ = store.RedisClient().Do(ctx, "SREM", store.ActiveRunsKey(), run)
}
if !reflect.DeepEqual(sets[0], sets[1]) {
t.Fatalf("scope=%v serial=%v channel=%v", scope, sets[0], sets[1])
}
if scope == nil {
want := []string{operation + ":1", operation + ":13", operation + ":2", operation + ":3"}
sort.Strings(want)
if !reflect.DeepEqual(sets[0], want) {
t.Fatalf("qualification set=%v want=%v", sets[0], want)
}
}
}
}
func TestChannelSQLShapeAndParameters(t *testing.T) {
for _, term := range []string{"FORCE INDEX (idx_card_chan_key)", "STRAIGHT_JOIN sys_dept", "c.chan_key = ?", "c.id > ?", "c.iccid <> ''", "c.current_package_code", "c.current_package_code <> ''", "d.deleted_at IS NULL", "c.tenant_id = d.tenant_id", "ORDER BY c.id ASC", "LIMIT ?"} {
if !strings.Contains(channelPageSQL, term) {
t.Fatalf("missing %q", term)
}
}
if strings.Contains(channelPageSQL, "<=") || strings.Count(channelPageSQL, "?") != 3 {
t.Fatal("SQL contains upper bound or changed bind parameter count")
}
}