package postgres import ( "context" "errors" "testing" "cryptoHermes/internal/entity" "github.com/pashagolub/pgxmock/v4" "github.com/stretchr/testify/require" ) func TestKlineRepo_UpsertMany_HappyPath(t *testing.T) { mock, err := pgxmock.NewPool() require.NoError(t, err) defer mock.Close() mock.ExpectBegin() mock.ExpectExec("INSERT INTO market_klines"). WithArgs( "binance", "BTCUSDT", "1h", int64(1700000000000), int64(1700003599999), "50000", "50500", "49800", "50200", "123", "6190000", int64(789), "60", "3000000", ). WillReturnResult(pgxmock.NewResult("INSERT", 1)) mock.ExpectCommit() mock.ExpectRollback() // defer Rollback after Commit is a no-op but mock sees it r := &KlineRepo{pool: mock} err = r.UpsertMany(context.Background(), []entity.Kline{{ Source: "binance", Symbol: "BTCUSDT", Interval: "1h", OpenTime: 1700000000000, CloseTime: 1700003599999, Open: "50000", High: "50500", Low: "49800", Close: "50200", Volume: "123", QuoteVolume: "6190000", TradeCount: 789, TakerBuyBaseVolume: "60", TakerBuyQuoteVolume: "3000000", IsClosed: true, }}) require.NoError(t, err) require.NoError(t, mock.ExpectationsWereMet()) } func TestKlineRepo_UpsertMany_EmptyShortCircuits(t *testing.T) { mock, err := pgxmock.NewPool() require.NoError(t, err) defer mock.Close() r := &KlineRepo{pool: mock} require.NoError(t, r.UpsertMany(context.Background(), nil)) require.NoError(t, mock.ExpectationsWereMet()) } func TestKlineRepo_UpsertMany_SkipsUnclosed(t *testing.T) { mock, err := pgxmock.NewPool() require.NoError(t, err) defer mock.Close() mock.ExpectBegin() // 第二根 K 线 IsClosed=false,应跳过;只 Exec 一次 mock.ExpectExec("INSERT INTO market_klines"). WithArgs( "binance", "BTCUSDT", "1h", int64(1), int64(2), "1", "1", "1", "1", "1", "1", int64(0), "", "", ). WillReturnResult(pgxmock.NewResult("INSERT", 1)) mock.ExpectCommit() mock.ExpectRollback() r := &KlineRepo{pool: mock} err = r.UpsertMany(context.Background(), []entity.Kline{ {Source: "binance", Symbol: "BTCUSDT", Interval: "1h", OpenTime: 1, CloseTime: 2, Open: "1", High: "1", Low: "1", Close: "1", Volume: "1", QuoteVolume: "1", IsClosed: true}, {Source: "binance", Symbol: "BTCUSDT", Interval: "1h", OpenTime: 3, CloseTime: 4, IsClosed: false}, // 未收线,跳过 }) require.NoError(t, err) require.NoError(t, mock.ExpectationsWereMet()) } func TestKlineRepo_UpsertMany_BeginError(t *testing.T) { mock, err := pgxmock.NewPool() require.NoError(t, err) defer mock.Close() mock.ExpectBegin().WillReturnError(errors.New("conn lost")) r := &KlineRepo{pool: mock} err = r.UpsertMany(context.Background(), []entity.Kline{{IsClosed: true}}) require.Error(t, err) require.Contains(t, err.Error(), "begin") require.NoError(t, mock.ExpectationsWereMet()) } func TestKlineRepo_UpsertMany_ExecError(t *testing.T) { mock, err := pgxmock.NewPool() require.NoError(t, err) defer mock.Close() mock.ExpectBegin() mock.ExpectExec("INSERT INTO market_klines"). WithArgs( pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), ). WillReturnError(errors.New("constraint violation")) mock.ExpectRollback() r := &KlineRepo{pool: mock} err = r.UpsertMany(context.Background(), []entity.Kline{{ Source: "binance", Symbol: "BTCUSDT", Interval: "1h", OpenTime: 1700000000000, IsClosed: true, }}) require.Error(t, err) require.Contains(t, err.Error(), "upsert kline") require.NoError(t, mock.ExpectationsWereMet()) } func TestKlineRepo_FindRecent_HappyPath(t *testing.T) { mock, err := pgxmock.NewPool() require.NoError(t, err) defer mock.Close() rows := mock.NewRows([]string{ "source", "symbol", "interval", "open_time", "close_time", "open", "high", "low", "close", "volume", "quote_volume", "trade_count", "taker_buy_base_volume", "taker_buy_quote_volume", }). // DB 返回时间倒序:t=2 先,t=1 后 AddRow("binance", "BTCUSDT", "1h", int64(2), int64(3), "50100", "50200", "50000", "50150", "10", "500000", int64(100), "5", "250000"). AddRow("binance", "BTCUSDT", "1h", int64(1), int64(2), "50000", "50100", "49900", "50050", "9", "450000", int64(90), "4", "200000") mock.ExpectQuery("SELECT source, symbol, interval"). WithArgs("BTCUSDT", "1h", 300). WillReturnRows(rows) r := &KlineRepo{pool: mock} got, err := r.FindRecent(context.Background(), "BTCUSDT", "1h", 0) // limit<=0 → 300 require.NoError(t, err) require.Len(t, got, 2) // 返回应已反序为升序:[1,2] require.Equal(t, int64(1), got[0].OpenTime) require.Equal(t, int64(2), got[1].OpenTime) require.True(t, got[0].IsClosed, "落库的都视为已收线") require.True(t, got[1].IsClosed) require.NoError(t, mock.ExpectationsWereMet()) } func TestKlineRepo_FindRecent_QueryError(t *testing.T) { mock, err := pgxmock.NewPool() require.NoError(t, err) defer mock.Close() mock.ExpectQuery("SELECT source, symbol, interval"). WithArgs("BTCUSDT", "1h", 100). WillReturnError(errors.New("timeout")) r := &KlineRepo{pool: mock} _, err = r.FindRecent(context.Background(), "BTCUSDT", "1h", 100) require.Error(t, err) require.Contains(t, err.Error(), "query") require.NoError(t, mock.ExpectationsWereMet()) }