337 lines
12 KiB
Go
337 lines
12 KiB
Go
package dashboard
|
|
|
|
import (
|
|
"context"
|
|
"database/sql/driver"
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
"hyapp-admin-server/internal/config"
|
|
)
|
|
|
|
func TestMySQLExternalDashboardSourceStatisticsOverview(t *testing.T) {
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("sqlmock: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
source := NewMySQLExternalDashboardSource(db, config.DashboardExternalSourceConfig{
|
|
Enabled: true,
|
|
AppCode: "Yumi",
|
|
AppName: "Yumi",
|
|
SysOrigin: "LIKEI",
|
|
StatTimezone: "Asia/Riyadh",
|
|
RequestTimeout: time.Second,
|
|
})
|
|
if source == nil {
|
|
t.Fatal("source is nil")
|
|
}
|
|
|
|
refreshedAt := time.Date(2026, 6, 30, 23, 0, 0, 0, time.UTC)
|
|
current := externalMetricRow(refreshedAt,
|
|
7, 40, 5,
|
|
"25.00", "100.00", "20.00", "0.00", "5.00", "120.00", "2.00",
|
|
"30.00", "11.00", "900.00", "300.00", 4, "100.00", "10.00",
|
|
"300.00", 8, "110.00",
|
|
3, 6, 2, 10, 1, 20,
|
|
)
|
|
previous := externalMetricRow(refreshedAt.Add(-24*time.Hour),
|
|
5, 20, 4,
|
|
"10.00", "50.00", "10.00", "0.00", "5.00", "60.00", "0.00",
|
|
"0.00", "0.00", "400.00", "100.00", 2, "40.00", "0.00",
|
|
"180.00", 5, "100.00",
|
|
1, 5, 1, 8, 0, 10,
|
|
)
|
|
|
|
expectExternalMetricQuery(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01").
|
|
WillReturnRows(sqlmock.NewRows(externalMetricColumns()).AddRow(current...))
|
|
expectExternalMetricQuery(mock, "Asia/Riyadh", "LIKEI", "2026-06-29", "2026-06-30").
|
|
WillReturnRows(sqlmock.NewRows(externalMetricColumns()).AddRow(previous...))
|
|
expectExternalMetricQuery(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01").
|
|
WillReturnRows(sqlmock.NewRows(externalMetricColumns("period_key")).AddRow(append([]driver.Value{"2026-06-30"}, current...)...))
|
|
expectExternalMetricQuery(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01").
|
|
WillReturnRows(sqlmock.NewRows(externalMetricColumns("country_code", "country_name")).AddRow(append([]driver.Value{"SA", "Saudi Arabia"}, current...)...))
|
|
expectExternalMetricQuery(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01").
|
|
WillReturnRows(sqlmock.NewRows(externalMetricColumns("period_key", "country_code", "country_name")).AddRow(append([]driver.Value{"2026-06-30", "SA", "Saudi Arabia"}, current...)...))
|
|
|
|
shanghai, err := time.LoadLocation("Asia/Shanghai")
|
|
if err != nil {
|
|
t.Fatalf("load location: %v", err)
|
|
}
|
|
start := time.Date(2026, 6, 30, 0, 0, 0, 0, shanghai)
|
|
overview, err := source.StatisticsOverview(context.Background(), StatisticsQuery{
|
|
AppCode: "yumi",
|
|
StatTZ: "Asia/Shanghai",
|
|
StartMS: start.UnixMilli(),
|
|
EndMS: start.AddDate(0, 0, 1).UnixMilli(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("StatisticsOverview: %v", err)
|
|
}
|
|
|
|
assertEqual(t, overview["app_code"], "yumi")
|
|
assertEqual(t, overview["stat_tz"], "Asia/Riyadh")
|
|
assertEqual(t, overview["request_stat_tz"], "Asia/Shanghai")
|
|
assertInt64(t, overview["recharge_usd_minor"], 12_500)
|
|
assertInt64(t, overview["arppu_usd_minor"], 2_500)
|
|
assertInt64(t, overview["registered_users"], 7)
|
|
assertInt64(t, overview["recharge_users"], 5)
|
|
assertFloat(t, overview["paid_conversion_rate"], 0.125)
|
|
assertFloat(t, overview["recharge_conversion_rate"], 0.125)
|
|
assertFloat(t, overview["payer_rate_delta_pp"], -7.5)
|
|
assertInt64(t, overview["game_turnover"], 300)
|
|
assertInt64(t, overview["game_profit"], 190)
|
|
assertFloat(t, overview["d1_retention_rate"], 0.5)
|
|
if overview["coin_total"] != nil {
|
|
t.Fatalf("coin_total should be nil, got %#v", overview["coin_total"])
|
|
}
|
|
if overview["salary_usd_minor"] != nil {
|
|
t.Fatalf("salary_usd_minor should be nil, got %#v", overview["salary_usd_minor"])
|
|
}
|
|
|
|
series, ok := overview["daily_series"].([]map[string]any)
|
|
if !ok || len(series) != 1 {
|
|
t.Fatalf("daily_series mismatch: %#v", overview["daily_series"])
|
|
}
|
|
assertEqual(t, series[0]["stat_day"], "2026-06-30")
|
|
assertInt64(t, series[0]["recharge_usd_minor"], 12_500)
|
|
|
|
countries, ok := overview["country_breakdown"].([]map[string]any)
|
|
if !ok || len(countries) != 1 {
|
|
t.Fatalf("country_breakdown mismatch: %#v", overview["country_breakdown"])
|
|
}
|
|
assertEqual(t, countries[0]["country_code"], "SA")
|
|
assertEqual(t, countries[0]["country_name"], "Saudi Arabia")
|
|
|
|
sources, ok := overview["report_metric_sources"].([]map[string]any)
|
|
if !ok {
|
|
t.Fatalf("report_metric_sources mismatch: %#v", overview["report_metric_sources"])
|
|
}
|
|
assertMetricSourceUnavailable(t, sources, "coin_total")
|
|
assertMetricSourceUnavailable(t, sources, "salary_usd_minor")
|
|
assertMetricSourceUnavailable(t, sources, "coin_seller_stock_coin")
|
|
assertMetricSourceUnavailable(t, sources, "avg_mic_online_ms")
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("sql expectations: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMySQLExternalDashboardSourceFiltersLegacyRegionCountries(t *testing.T) {
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("sqlmock: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
source := NewMySQLExternalDashboardSource(db, config.DashboardExternalSourceConfig{
|
|
Enabled: true,
|
|
AppCode: "Yumi",
|
|
AppName: "Yumi",
|
|
SysOrigin: "LIKEI",
|
|
StatTimezone: "Asia/Riyadh",
|
|
RequestTimeout: time.Second,
|
|
}, staticDashboardRegionResolver{appCode: "yumi", regionID: 1001, countryCodes: []string{"SA", "AE"}})
|
|
if source == nil {
|
|
t.Fatal("source is nil")
|
|
}
|
|
|
|
refreshedAt := time.Date(2026, 6, 30, 23, 0, 0, 0, time.UTC)
|
|
current := externalMetricRow(refreshedAt,
|
|
7, 40, 5,
|
|
"25.00", "100.00", "20.00", "0.00", "5.00", "120.00", "2.00",
|
|
"30.00", "11.00", "900.00", "300.00", 4, "100.00", "10.00",
|
|
"300.00", 8, "110.00",
|
|
3, 6, 2, 10, 1, 20,
|
|
)
|
|
previous := externalMetricRow(refreshedAt.Add(-24*time.Hour),
|
|
5, 20, 4,
|
|
"10.00", "50.00", "10.00", "0.00", "5.00", "60.00", "0.00",
|
|
"0.00", "0.00", "400.00", "100.00", 2, "40.00", "0.00",
|
|
"180.00", 5, "100.00",
|
|
1, 5, 1, 8, 0, 10,
|
|
)
|
|
|
|
expectExternalMetricQueryWithCountries(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01", "AE", "SA").
|
|
WillReturnRows(sqlmock.NewRows(externalMetricColumns()).AddRow(current...))
|
|
expectExternalMetricQueryWithCountries(mock, "Asia/Riyadh", "LIKEI", "2026-06-29", "2026-06-30", "AE", "SA").
|
|
WillReturnRows(sqlmock.NewRows(externalMetricColumns()).AddRow(previous...))
|
|
expectExternalMetricQueryWithCountries(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01", "AE", "SA").
|
|
WillReturnRows(sqlmock.NewRows(externalMetricColumns("period_key")).AddRow(append([]driver.Value{"2026-06-30"}, current...)...))
|
|
expectExternalMetricQueryWithCountries(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01", "AE", "SA").
|
|
WillReturnRows(sqlmock.NewRows(externalMetricColumns("country_code", "country_name")).AddRow(append([]driver.Value{"SA", "Saudi Arabia"}, current...)...))
|
|
expectExternalMetricQueryWithCountries(mock, "Asia/Riyadh", "LIKEI", "2026-06-30", "2026-07-01", "AE", "SA").
|
|
WillReturnRows(sqlmock.NewRows(externalMetricColumns("period_key", "country_code", "country_name")).AddRow(append([]driver.Value{"2026-06-30", "SA", "Saudi Arabia"}, current...)...))
|
|
|
|
shanghai, err := time.LoadLocation("Asia/Shanghai")
|
|
if err != nil {
|
|
t.Fatalf("load location: %v", err)
|
|
}
|
|
start := time.Date(2026, 6, 30, 0, 0, 0, 0, shanghai)
|
|
overview, err := source.StatisticsOverview(context.Background(), StatisticsQuery{
|
|
AppCode: "yumi",
|
|
StatTZ: "Asia/Shanghai",
|
|
StartMS: start.UnixMilli(),
|
|
EndMS: start.AddDate(0, 0, 1).UnixMilli(),
|
|
RegionID: 1001,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("StatisticsOverview: %v", err)
|
|
}
|
|
countries, ok := overview["country_breakdown"].([]map[string]any)
|
|
if !ok || len(countries) != 1 {
|
|
t.Fatalf("country_breakdown mismatch: %#v", overview["country_breakdown"])
|
|
}
|
|
assertEqual(t, countries[0]["region_id"], int64(1001))
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatalf("sql expectations: %v", err)
|
|
}
|
|
}
|
|
|
|
func expectExternalMetricQuery(mock sqlmock.Sqlmock, statTimezone string, sysOrigin string, startDate string, endDate string) *sqlmock.ExpectedQuery {
|
|
return mock.ExpectQuery("FROM country_dashboard_period_metric").
|
|
WithArgs(statTimezone, sysOrigin, startDate, endDate)
|
|
}
|
|
|
|
func expectExternalMetricQueryWithCountries(mock sqlmock.Sqlmock, statTimezone string, sysOrigin string, startDate string, endDate string, countryCodes ...string) *sqlmock.ExpectedQuery {
|
|
args := []driver.Value{statTimezone, sysOrigin, startDate, endDate}
|
|
for _, countryCode := range countryCodes {
|
|
args = append(args, countryCode)
|
|
}
|
|
return mock.ExpectQuery("FROM country_dashboard_period_metric").
|
|
WithArgs(args...)
|
|
}
|
|
|
|
type staticDashboardRegionResolver struct {
|
|
appCode string
|
|
regionID int64
|
|
countryCodes []string
|
|
}
|
|
|
|
func (r staticDashboardRegionResolver) CountryCodesForRegion(_ context.Context, appCode string, regionID int64) ([]string, bool, error) {
|
|
if r.appCode != appCode || r.regionID != regionID {
|
|
return nil, false, nil
|
|
}
|
|
return r.countryCodes, true, nil
|
|
}
|
|
|
|
func externalMetricColumns(prefix ...string) []string {
|
|
columns := []string{
|
|
"country_new_user",
|
|
"daily_active_user",
|
|
"daily_recharge_user",
|
|
"new_user_recharge",
|
|
"official_recharge",
|
|
"mifapay_recharge",
|
|
"google_recharge",
|
|
"dealer_recharge",
|
|
"user_recharge",
|
|
"new_dealer_user_recharge",
|
|
"salary_exchange",
|
|
"salary_transfer",
|
|
"gift_consume",
|
|
"lucky_gift_total_flow",
|
|
"lucky_gift_user",
|
|
"lucky_gift_payout",
|
|
"lucky_gift_anchor_share",
|
|
"game_total_flow",
|
|
"game_user",
|
|
"game_payout",
|
|
"d1_retention_user",
|
|
"d1_retention_base_user",
|
|
"d7_retention_user",
|
|
"d7_retention_base_user",
|
|
"d30_retention_user",
|
|
"d30_retention_base_user",
|
|
"refreshed_at",
|
|
}
|
|
return append(prefix, columns...)
|
|
}
|
|
|
|
func externalMetricRow(refreshedAt time.Time,
|
|
newUsers int64, activeUsers int64, paidUsers int64,
|
|
newUserRecharge string, officialRecharge string, mifaPayRecharge string, googleRecharge string, dealerRecharge string, userRecharge string, newDealerRecharge string,
|
|
salaryExchange string, salaryTransfer string, giftConsume string, luckyGiftTurnover string, luckyGiftPayers int64, luckyGiftPayout string, luckyGiftAnchorShare string,
|
|
gameTurnover string, gamePlayers int64, gamePayout string,
|
|
d1Users int64, d1Base int64, d7Users int64, d7Base int64, d30Users int64, d30Base int64,
|
|
) []driver.Value {
|
|
return []driver.Value{
|
|
newUsers,
|
|
activeUsers,
|
|
paidUsers,
|
|
newUserRecharge,
|
|
officialRecharge,
|
|
mifaPayRecharge,
|
|
googleRecharge,
|
|
dealerRecharge,
|
|
userRecharge,
|
|
newDealerRecharge,
|
|
salaryExchange,
|
|
salaryTransfer,
|
|
giftConsume,
|
|
luckyGiftTurnover,
|
|
luckyGiftPayers,
|
|
luckyGiftPayout,
|
|
luckyGiftAnchorShare,
|
|
gameTurnover,
|
|
gamePlayers,
|
|
gamePayout,
|
|
d1Users,
|
|
d1Base,
|
|
d7Users,
|
|
d7Base,
|
|
d30Users,
|
|
d30Base,
|
|
refreshedAt,
|
|
}
|
|
}
|
|
|
|
func assertEqual(t *testing.T, got any, want any) {
|
|
t.Helper()
|
|
if got != want {
|
|
t.Fatalf("got %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func assertInt64(t *testing.T, got any, want int64) {
|
|
t.Helper()
|
|
value, ok := got.(int64)
|
|
if !ok {
|
|
t.Fatalf("got %T(%#v), want int64(%d)", got, got, want)
|
|
}
|
|
if value != want {
|
|
t.Fatalf("got %d, want %d", value, want)
|
|
}
|
|
}
|
|
|
|
func assertFloat(t *testing.T, got any, want float64) {
|
|
t.Helper()
|
|
value, ok := got.(float64)
|
|
if !ok {
|
|
t.Fatalf("got %T(%#v), want float64(%f)", got, got, want)
|
|
}
|
|
if math.Abs(value-want) > 0.000001 {
|
|
t.Fatalf("got %f, want %f", value, want)
|
|
}
|
|
}
|
|
|
|
func assertMetricSourceUnavailable(t *testing.T, sources []map[string]any, field string) {
|
|
t.Helper()
|
|
for _, source := range sources {
|
|
if source["field"] != field {
|
|
continue
|
|
}
|
|
if source["available"] != false {
|
|
t.Fatalf("%s should be unavailable: %#v", field, source)
|
|
}
|
|
if source["missing_reason"] == "" {
|
|
t.Fatalf("%s missing reason is empty: %#v", field, source)
|
|
}
|
|
return
|
|
}
|
|
t.Fatalf("metric source %s not found: %#v", field, sources)
|
|
}
|