package repository import ( "code.30cm.net/digimon/app-cloudep-wallet-service/internal/config" "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/repository" "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/domain/wallet" "code.30cm.net/digimon/app-cloudep-wallet-service/pkg/lib/sql_client" "context" "errors" "fmt" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" "gorm.io/gorm" "testing" "time" ) func SetupTestWalletRepository() (repository.WalletRepository, *gorm.DB, func(), error) { host, port, _, tearDown, err := startMySQLContainer() if err != nil { return nil, nil, nil, fmt.Errorf("failed to start MySQL container: %w", err) } conf := config.Config{ MySQL: struct { UserName string Password string Host string Port string Database string MaxIdleConns int MaxOpenConns int ConnMaxLifetime time.Duration LogLevel string }{ UserName: MySQLUser, Password: MySQLPassword, Host: host, Port: port, Database: MySQLDatabase, MaxIdleConns: 10, MaxOpenConns: 100, ConnMaxLifetime: 300, LogLevel: "info", }, } db, err := sql_client.NewMySQLClient(conf) if err != nil { tearDown() return nil, nil, nil, fmt.Errorf("failed to create db client: %w", err) } repo := MustCategoryRepository(WalletRepositoryParam{DB: db}) return repo, db, tearDown, nil } func TestWalletRepository_InitWallets(t *testing.T) { repo, db, tearDown, err := SetupTestWalletRepository() assert.NoError(t, err) defer tearDown() // 🔽 這裡加上建表 SQL createTableSQL := ` CREATE TABLE IF NOT EXISTS wallet ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主鍵 ID(錢包唯一識別)', brand VARCHAR(50) NOT NULL COMMENT '品牌/平台(多租戶識別)', uid VARCHAR(64) NOT NULL COMMENT '使用者 UID', asset VARCHAR(32) NOT NULL COMMENT '資產代碼(如 BTC、ETH、GEM_RED 等)', balance DECIMAL(30, 18) UNSIGNED NOT NULL DEFAULT 0 COMMENT '資產餘額', type TINYINT NOT NULL COMMENT '錢包類型', create_at INTEGER NOT NULL DEFAULT 0 COMMENT '建立時間(Unix)', update_at INTEGER NOT NULL DEFAULT 0 COMMENT '更新時間(Unix)', PRIMARY KEY (id), UNIQUE KEY uq_brand_uid_asset_type (brand, uid, asset, type), KEY idx_uid (uid), KEY idx_brand (brand) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='錢包'; ` err = db.Exec(createTableSQL).Error assert.NoError(t, err) tests := []struct { name string param []repository.Wallet wantErr bool }{ { name: "insert single wallet", param: []repository.Wallet{ { Brand: "test-brand", UID: "user001", Asset: "BTC", Balance: decimal.NewFromFloat(10.5), Type: wallet.TypeAvailable, }, }, wantErr: false, }, { name: "insert multiple wallets", param: []repository.Wallet{ { Brand: "test-brand", UID: "user002", Asset: "ETH", Balance: decimal.NewFromFloat(5), Type: wallet.TypeAvailable, }, { Brand: "test-brand", UID: "user002", Asset: "ETH", Balance: decimal.NewFromFloat(1.2), Type: wallet.TypeFreeze, }, }, wantErr: false, }, { name: "insert duplicate primary key (should fail if unique constraint)", param: []repository.Wallet{ { Brand: "test-brand", UID: "user001", Asset: "BTC", Balance: decimal.NewFromFloat(1), Type: wallet.TypeAvailable, // 與第一筆測試資料相同 }, }, wantErr: true, // 預期會違反 UNIQUE constraint }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := repo.InitWallets(context.Background(), tt.param) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } func TestWalletRepository_QueryBalances(t *testing.T) { repo, db, tearDown, err := SetupTestWalletRepository() assert.NoError(t, err) defer tearDown() // 🔽 這裡加上建表 SQL createTableSQL := ` CREATE TABLE IF NOT EXISTS wallet ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主鍵 ID(錢包唯一識別)', brand VARCHAR(50) NOT NULL COMMENT '品牌/平台(多租戶識別)', uid VARCHAR(64) NOT NULL COMMENT '使用者 UID', asset VARCHAR(32) NOT NULL COMMENT '資產代碼(如 BTC、ETH、GEM_RED 等)', balance DECIMAL(30, 18) UNSIGNED NOT NULL DEFAULT 0 COMMENT '資產餘額', type TINYINT NOT NULL COMMENT '錢包類型', create_at INTEGER NOT NULL DEFAULT 0 COMMENT '建立時間(Unix)', update_at INTEGER NOT NULL DEFAULT 0 COMMENT '更新時間(Unix)', PRIMARY KEY (id), UNIQUE KEY uq_brand_uid_asset_type (brand, uid, asset, type), KEY idx_uid (uid), KEY idx_brand (brand) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='錢包'; ` err = db.Exec(createTableSQL).Error assert.NoError(t, err) ctx := context.Background() // 建立初始化錢包資料 wallets := []repository.Wallet{ {Brand: "brand1", UID: "user1", Asset: "BTC", Balance: decimal.NewFromFloat(1.5), Type: wallet.TypeAvailable}, {Brand: "brand1", UID: "user1", Asset: "ETH", Balance: decimal.NewFromFloat(2.5), Type: wallet.TypeFreeze}, {Brand: "brand1", UID: "user2", Asset: "BTC", Balance: decimal.NewFromFloat(3.0), Type: wallet.TypeAvailable}, } err = repo.InitWallets(ctx, wallets) assert.NoError(t, err) tests := []struct { name string query repository.BalanceQuery expected int }{ { name: "Query all by UID", query: repository.BalanceQuery{ UID: "user1", }, expected: 2, }, { name: "Query by UID and Asset", query: repository.BalanceQuery{ UID: "user1", Asset: "BTC", }, expected: 1, }, { name: "Query by UID with non-existing asset", query: repository.BalanceQuery{ UID: "user1", Asset: "GEM_RED", }, expected: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := repo.QueryBalances(ctx, tt.query) assert.NoError(t, err) assert.Len(t, got, tt.expected) }) } } func TestWalletRepository_QueryBalancesByUIDs(t *testing.T) { repo, db, tearDown, err := SetupTestWalletRepository() assert.NoError(t, err) defer tearDown() // 🔽 這裡加上建表 SQL createTableSQL := ` CREATE TABLE IF NOT EXISTS wallet ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主鍵 ID(錢包唯一識別)', brand VARCHAR(50) NOT NULL COMMENT '品牌/平台(多租戶識別)', uid VARCHAR(64) NOT NULL COMMENT '使用者 UID', asset VARCHAR(32) NOT NULL COMMENT '資產代碼(如 BTC、ETH、GEM_RED 等)', balance DECIMAL(30, 18) UNSIGNED NOT NULL DEFAULT 0 COMMENT '資產餘額', type TINYINT NOT NULL COMMENT '錢包類型', create_at INTEGER NOT NULL DEFAULT 0 COMMENT '建立時間(Unix)', update_at INTEGER NOT NULL DEFAULT 0 COMMENT '更新時間(Unix)', PRIMARY KEY (id), UNIQUE KEY uq_brand_uid_asset_type (brand, uid, asset, type), KEY idx_uid (uid), KEY idx_brand (brand) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='錢包'; ` err = db.Exec(createTableSQL).Error assert.NoError(t, err) ctx := context.Background() // 初始化錢包資料 initData := []repository.Wallet{ {Brand: "brand1", UID: "user1", Asset: "BTC", Balance: decimal.NewFromFloat(1.5), Type: wallet.TypeAvailable}, {Brand: "brand1", UID: "user2", Asset: "BTC", Balance: decimal.NewFromFloat(2.0), Type: wallet.TypeAvailable}, {Brand: "brand1", UID: "user2", Asset: "ETH", Balance: decimal.NewFromFloat(3.0), Type: wallet.TypeFreeze}, {Brand: "brand1", UID: "user3", Asset: "BTC", Balance: decimal.NewFromFloat(4.5), Type: wallet.TypeAvailable}, } err = repo.InitWallets(ctx, initData) assert.NoError(t, err) tests := []struct { name string uids []string query repository.BalanceQuery expected int }{ { name: "Query all users with BTC", uids: []string{"user1", "user2", "user3"}, query: repository.BalanceQuery{Asset: "BTC"}, expected: 3, }, { name: "Query specific users with filter by type", uids: []string{"user2"}, query: repository.BalanceQuery{Kinds: []wallet.Types{wallet.TypeAvailable}}, expected: 1, }, { name: "Query with no matches", uids: []string{"user2"}, query: repository.BalanceQuery{Asset: "DOGE"}, expected: 0, }, { name: "Query all for user2", uids: []string{"user2"}, query: repository.BalanceQuery{}, expected: 2, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := repo.QueryBalancesByUIDs(ctx, tt.uids, tt.query) assert.NoError(t, err) assert.Len(t, result, tt.expected) }) } } func TestWalletRepository_NewDB(t *testing.T) { repo, _, tearDown, err := SetupTestWalletRepository() assert.NoError(t, err) defer tearDown() tx := repo.NewDB() assert.NotNil(t, tx) assert.NoError(t, tx.Commit().Error) } func TestWalletRepository_Transaction(t *testing.T) { repo, _, tearDown, err := SetupTestWalletRepository() assert.NoError(t, err) defer tearDown() t.Run("commit success", func(t *testing.T) { err := repo.Transaction(func(tx *gorm.DB) error { return nil // 模擬成功流程 }) assert.NoError(t, err) }) t.Run("rollback due to fn error", func(t *testing.T) { customErr := errors.New("rollback me") err := repo.Transaction(func(tx *gorm.DB) error { return customErr }) assert.ErrorIs(t, err, customErr) }) } func TestWalletRepository_Session(t *testing.T) { repo, _, tearDown, err := SetupTestWalletRepository() assert.NoError(t, err) defer tearDown() sess := repo.Session("userX", "BTC") assert.NotNil(t, sess) } func TestWalletRepository_SessionWithTx(t *testing.T) { repo, _, tearDown, err := SetupTestWalletRepository() assert.NoError(t, err) defer tearDown() tx := repo.NewDB() defer tx.Rollback() sess := repo.SessionWithTx(tx, "userY", "ETH") assert.NotNil(t, sess) }