package coinglass import ( "context" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "net/url" "strconv" "sync/atomic" "testing" "time" "github.com/stretchr/testify/require" ) type netHTTPDoer struct { client *http.Client } func (d netHTTPDoer) Do(ctx context.Context, req httpRequest) (*httpResponse, error) { httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, req.URL, nil) if err != nil { return nil, err } httpReq.Header = req.Header.Clone() resp, err := d.client.Do(httpReq) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return &httpResponse{ StatusCode: resp.StatusCode, Header: resp.Header.Clone(), Body: body, }, nil } func TestClientGetPlaintext_HappyPath(t *testing.T) { const ( path = "/api/index/v2/liqHeatMap" token = "dfa89d5d00cf4000" ) plaintext := `{"instrument":{"exName":"Binance","instrumentId":"BTCUSDT"},"liq":[[1,2,3.4]]}` ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, path, r.URL.Path) require.Equal(t, "true", r.URL.Query().Get("merge")) require.Equal(t, "Binance_BTCUSDT", r.URL.Query().Get("symbol")) require.NotEmpty(t, r.URL.Query().Get("data")) require.Equal(t, "true", r.Header.Get("Encryption")) require.Equal(t, defaultOrigin, r.Header.Get("Origin")) require.NotEmpty(t, r.Header.Get("Cache-Ts-V2")) cacheTs := r.Header.Get("Cache-Ts-V2") _, err := strconv.ParseInt(cacheTs, 10, 64) require.NoError(t, err) w.Header().Set("v", "0") w.Header().Set("user", encryptForTest(t, userKeyFromSeed(cacheTs), token)) _ = json.NewEncoder(w).Encode(map[string]any{ "code": "0", "msg": "success", "data": encryptForTest(t, bodyKeyFromToken(token), plaintext), }) })) defer ts.Close() client := newTestClient(t, ts.URL) got, err := client.GetPlaintext(context.Background(), path, url.Values{ "merge": {"true"}, "symbol": {"Binance_BTCUSDT"}, "interval": {"5"}, "limit": {"288"}, }) require.NoError(t, err) require.JSONEq(t, plaintext, string(got)) } func TestClientGetPlaintext_RetriesFakeSuccessOnce(t *testing.T) { var attempts int32 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&attempts, 1) _ = json.NewEncoder(w).Encode(map[string]any{ "success": true, "code": "0", "msg": "success", "data": nil, }) })) defer ts.Close() client := newTestClient(t, ts.URL) _, err := client.GetPlaintext(context.Background(), "/api/index/v2/liqHeatMap", nil) require.ErrorIs(t, err, ErrFakeSuccess) require.Equal(t, int32(2), atomic.LoadInt32(&attempts)) } func TestClientGetPlaintext_BusinessErrorDoesNotRetry(t *testing.T) { var attempts int32 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&attempts, 1) _ = json.NewEncoder(w).Encode(map[string]any{ "success": false, "code": "40000", "msg": "40000", }) })) defer ts.Close() client := newTestClient(t, ts.URL) _, err := client.GetPlaintext(context.Background(), "/api/index/v2/liqHeatMap", nil) require.Error(t, err) require.True(t, errors.Is(err, ErrAPIBusiness)) require.Equal(t, int32(1), atomic.LoadInt32(&attempts)) } func TestClientGetPlaintext_MissingDecryptHeadersIsFakeSuccess(t *testing.T) { const token = "dfa89d5d00cf4000" var attempts int32 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&attempts, 1) _ = json.NewEncoder(w).Encode(map[string]any{ "code": "0", "msg": "success", "data": encryptForTest(t, bodyKeyFromToken(token), `{"ok":true}`), }) })) defer ts.Close() client := newTestClient(t, ts.URL) _, err := client.GetPlaintext(context.Background(), "/api/index/v2/liqHeatMap", nil) require.ErrorIs(t, err, ErrFakeSuccess) require.Equal(t, int32(2), atomic.LoadInt32(&attempts)) } func newTestClient(t *testing.T, baseURL string) *Client { t.Helper() client, err := newClientWithDoer(ClientOptions{ BaseURL: baseURL, TOTPSecret: "JBSWY3DPEHPK3PXP", FakeSuccessRetryWait: time.Nanosecond, }, netHTTPDoer{client: http.DefaultClient}) require.NoError(t, err) client.now = func() time.Time { return time.Unix(1735689600, 123*int64(time.Millisecond)) } return client }