package binance import ( "context" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/config" "hyapp/services/wallet-service/internal/service/wallet/ports" ) func TestFindUSDTDepositMatchesOffchainTransferAndSignedRequest(t *testing.T) { var sawDepositRequest bool server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v3/time": _, _ = w.Write([]byte(`{"serverTime":1783345000000}`)) case "/sapi/v1/capital/deposit/hisrec": sawDepositRequest = true if r.Header.Get("X-MBX-APIKEY") != "api-lalu" { t.Fatalf("X-MBX-APIKEY mismatch") } values := r.URL.Query() signature := values.Get("signature") values.Del("signature") if signature != signQueryString(values.Encode(), "secret-lalu") { t.Fatalf("signature mismatch: query=%s signature=%s", values.Encode(), signature) } if values.Get("timestamp") != "1783345000000" || values.Get("recvWindow") != "10000" || values.Get("coin") != "USDT" || values.Get("limit") != "1000" { t.Fatalf("signed deposit query mismatch: %s", values.Encode()) } if values.Get("endTime") != "1783345000000" || values.Get("startTime") != "1775569000000" { t.Fatalf("deposit bounded window mismatch: %s", values.Encode()) } _, _ = w.Write([]byte(`[{ "id":"deposit-1", "amount":"500.00000000", "coin":"USDT", "network":"TRX", "status":1, "address":"TNwqCkF8cnM4XiwjnbbKTfdZvFDqVoa1TN", "txId":"Off-chain transfer 388611987194", "transferType":1, "insertTime":1783344000000, "completeTime":1783344025000, "confirmTimes":"1/1", "unlockConfirm":0, "walletType":0 }]`)) default: http.NotFound(w, r) } })) defer server.Close() client := New(config.BinanceConfig{ Enabled: true, APIBaseURL: server.URL, RecvWindow: 10000, HTTPTimeout: time.Second, Lalu: config.BinanceAccountConfig{ APIKey: "api-lalu", APISecretKey: "secret-lalu", }, }) record, err := client.FindUSDTDeposit(context.Background(), ports.BinanceDepositLookupRequest{ AppCode: "lalu", ExternalID: "388611987194", Coin: "USDT", Network: "TRX", ToAddress: "TNwqCkF8cnM4XiwjnbbKTfdZvFDqVoa1TN", AmountMinor: 50000, }) if err != nil { t.Fatalf("FindUSDTDeposit failed: %v", err) } if !sawDepositRequest { t.Fatalf("deposit history endpoint was not called") } if record.TxID != "Off-chain transfer 388611987194" || record.AmountMinor != 50000 || record.Network != "TRX" || !strings.Contains(record.RawJSON, `"txId"`) { t.Fatalf("deposit record mismatch: %+v", record) } } func TestFindUSDTDepositRejectsMismatches(t *testing.T) { baseReq := ports.BinanceDepositLookupRequest{ AppCode: "lalu", ExternalID: "388611987194", Coin: "USDT", Network: "TRX", ToAddress: "TNwqCkF8cnM4XiwjnbbKTfdZvFDqVoa1TN", AmountMinor: 50000, } baseRecord := depositRecord{ Amount: "500", Coin: "USDT", Network: "TRX", Status: 1, Address: "TNwqCkF8cnM4XiwjnbbKTfdZvFDqVoa1TN", TxID: "Off-chain transfer 388611987194", } cases := []struct { name string mutate func(*depositRecord) message string }{ {name: "status", mutate: func(r *depositRecord) { r.Status = 0 }, message: "status is not successful"}, {name: "network", mutate: func(r *depositRecord) { r.Network = "BSC" }, message: "network mismatch"}, {name: "address", mutate: func(r *depositRecord) { r.Address = "TDifferentAddress" }, message: "address mismatch"}, {name: "amount", mutate: func(r *depositRecord) { r.Amount = "499.99" }, message: "amount mismatch"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { record := baseRecord tc.mutate(&record) _, err := matchDepositRecord(baseReq, []depositRecord{record}) if !xerr.IsCode(err, xerr.Conflict) || !strings.Contains(err.Error(), tc.message) { t.Fatalf("expected conflict %q, got %v", tc.message, err) } }) } } func TestFindUSDTDepositMissingAccountReturnsConfiguredError(t *testing.T) { client := New(config.BinanceConfig{ Enabled: true, APIBaseURL: "https://binance.invalid", Lalu: config.BinanceAccountConfig{ APIKey: "api-lalu", APISecretKey: "secret-lalu", }, }) _, err := client.FindUSDTDeposit(context.Background(), ports.BinanceDepositLookupRequest{ AppCode: "aslan", ExternalID: "388611987194", Coin: "USDT", Network: "TRX", ToAddress: "TNwqCkF8cnM4XiwjnbbKTfdZvFDqVoa1TN", AmountMinor: 50000, }) if !xerr.IsCode(err, xerr.Unavailable) || !strings.Contains(err.Error(), "binance account is not configured") { t.Fatalf("missing account should be explicit, got %v", err) } } func TestDepositTxIDMatchesOffchainVariants(t *testing.T) { if !depositTxIDMatches("Off-chain transfer 388611987194", "388611987194") { t.Fatalf("plain off-chain id should match Binance txId") } if !depositTxIDMatches("Off-chain transfer 388611987194", "Off-chain transfer 388611987194") { t.Fatalf("full off-chain txId should match itself") } if depositTxIDMatches("Off-chain transfer 388611987194", "388611987195") { t.Fatalf("different off-chain id must not match") } } func TestSignQueryStringStable(t *testing.T) { values := url.Values{} values.Set("coin", "USDT") values.Set("timestamp", "1783345000000") signature := signQueryString(values.Encode(), "secret-lalu") if signature != signQueryString("coin=USDT×tamp=1783345000000", "secret-lalu") { t.Fatalf("signature must be based on encoded query string") } }