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

356 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}