356 lines
10 KiB
Go
356 lines
10 KiB
Go
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)
|
||
}
|