app-cloudep-wallet-service/pkg/repository/wallet_test.go

356 lines
10 KiB
Go
Raw Normal View History

2025-04-16 09:24:54 +00:00
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 '資產代碼 BTCETHGEM_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 '資產代碼 BTCETHGEM_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 '資產代碼 BTCETHGEM_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)
}