mirror of
https://github.com/status-im/eth-rpc-proxy.git
synced 2026-08-27 17:51:11 +00:00
696 lines
20 KiB
Go
696 lines
20 KiB
Go
package httpserver
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/mock/gomock"
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zaptest"
|
|
|
|
"github.com/status-im/proxy-common/models"
|
|
|
|
"go-proxy-cache/internal/cache"
|
|
"go-proxy-cache/internal/cache/service"
|
|
"go-proxy-cache/internal/interfaces/mock"
|
|
localModels "go-proxy-cache/internal/models"
|
|
"go-proxy-cache/internal/utils"
|
|
|
|
cachemock "github.com/status-im/proxy-common/cache/mock"
|
|
)
|
|
|
|
// Note: Metrics are now package-level variables in the metrics package
|
|
|
|
// mockCache implements the Cache interface for testing
|
|
type mockCache struct {
|
|
data map[string]*models.CacheEntry
|
|
hits map[string]bool
|
|
}
|
|
|
|
func newMockCache() *mockCache {
|
|
return &mockCache{
|
|
data: make(map[string]*models.CacheEntry),
|
|
hits: make(map[string]bool),
|
|
}
|
|
}
|
|
|
|
func (m *mockCache) Get(key string) (*models.CacheEntry, bool) {
|
|
entry, found := m.data[key]
|
|
if found {
|
|
m.hits[key] = true
|
|
}
|
|
return entry, found
|
|
}
|
|
|
|
func (m *mockCache) GetStale(key string) (*models.CacheEntry, bool) {
|
|
entry, found := m.data[key]
|
|
return entry, found
|
|
}
|
|
|
|
func (m *mockCache) Set(key string, val []byte, ttl models.TTL) {
|
|
now := time.Now().Unix()
|
|
m.data[key] = &models.CacheEntry{
|
|
Data: val,
|
|
CreatedAt: now,
|
|
StaleAt: now + int64(ttl.Fresh.Seconds()),
|
|
ExpiresAt: now + int64(ttl.Fresh.Seconds()) + int64(ttl.Stale.Seconds()),
|
|
}
|
|
}
|
|
|
|
func (m *mockCache) Delete(key string) {
|
|
delete(m.data, key)
|
|
}
|
|
|
|
// setupMockCacheClassifier configures the mock cache classifier with common expectations
|
|
func setupMockCacheClassifier(ctrl *gomock.Controller) *mock.MockCacheRulesClassifier {
|
|
mockClassifier := mock.NewMockCacheRulesClassifier(ctrl)
|
|
|
|
// Set up common expectations
|
|
mockClassifier.EXPECT().GetTtl(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
|
|
func(chain, network string, request *localModels.JSONRPCRequest) models.CacheInfo {
|
|
switch request.Method {
|
|
case "eth_getBlockByHash":
|
|
return models.CacheInfo{TTL: 86400 * time.Second, CacheType: "permanent"}
|
|
case "eth_blockNumber":
|
|
return models.CacheInfo{TTL: 5 * time.Second, CacheType: "short"}
|
|
default:
|
|
return models.CacheInfo{TTL: 0, CacheType: "none"}
|
|
}
|
|
},
|
|
).AnyTimes()
|
|
|
|
// Set up ShouldSkipNullCache expectation - return true for methods that should skip null caching
|
|
mockClassifier.EXPECT().ShouldSkipNullCache(gomock.Any()).DoAndReturn(
|
|
func(method string) bool {
|
|
switch method {
|
|
case "eth_getTransactionReceipt", "eth_getTransactionByHash", "eth_getBlockByHash", "eth_getBlockByNumber":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
},
|
|
).AnyTimes()
|
|
|
|
return mockClassifier
|
|
}
|
|
|
|
// setupCacheService creates a cache service with mock caches for testing
|
|
func setupCacheService(ctrl *gomock.Controller, logger *zap.Logger) (*service.CacheService, *mockCache, *mockCache) {
|
|
l1Cache := newMockCache()
|
|
l2Cache := newMockCache()
|
|
cacheClassifier := setupMockCacheClassifier(ctrl)
|
|
|
|
mockMetrics := cachemock.NewMockMetricsRecorder(ctrl)
|
|
mockMetrics.EXPECT().TimeCacheOperation(gomock.Any(), gomock.Any()).Return(func() {}).AnyTimes()
|
|
mockMetrics.EXPECT().RecordCacheMiss(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
|
mockMetrics.EXPECT().RecordCacheHit(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
|
mockMetrics.EXPECT().RecordCacheSet(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
|
mockMetrics.EXPECT().RecordCacheBytesRead(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
|
|
|
// Create cache service with mocked dependencies
|
|
cacheService := service.NewCacheService(l1Cache, l2Cache, cacheClassifier, false, logger, mockMetrics)
|
|
|
|
return cacheService, l1Cache, l2Cache
|
|
}
|
|
|
|
// generateCacheKey generates the actual cache key using the real key builder
|
|
func generateCacheKey(chain, network, rawBody string) (string, error) {
|
|
keyBuilder := cache.NewKeyBuilder()
|
|
request, err := utils.ParseJSONRPCRequest(rawBody)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return keyBuilder.Build(chain, network, request)
|
|
}
|
|
|
|
func TestServer_HandleGet(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
logger := zaptest.NewLogger(t)
|
|
cacheService, l1Cache, l2Cache := setupCacheService(ctrl, logger)
|
|
|
|
server := NewServer(cacheService, logger)
|
|
|
|
tests := []struct {
|
|
name string
|
|
requestBody CacheRequest
|
|
setupCache func()
|
|
expectedStatus int
|
|
expectedFound bool
|
|
}{
|
|
{
|
|
name: "cache miss",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_blockNumber","params":[],"jsonrpc":"2.0","id":1}`,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
expectedFound: false,
|
|
},
|
|
{
|
|
name: "cache hit L1",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_blockNumber","params":[],"jsonrpc":"2.0","id":1}`,
|
|
},
|
|
setupCache: func() {
|
|
key, _ := generateCacheKey("ethereum", "mainnet", `{"method":"eth_blockNumber","params":[],"jsonrpc":"2.0","id":1}`)
|
|
l1Cache.Set(key, []byte(`{"result":"0x123","id":1}`), models.TTL{Fresh: time.Hour, Stale: time.Minute})
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
expectedFound: true,
|
|
},
|
|
{
|
|
name: "cache hit L2",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_getBlockByHash","params":["0x123",true],"jsonrpc":"2.0","id":2}`,
|
|
},
|
|
setupCache: func() {
|
|
key, _ := generateCacheKey("ethereum", "mainnet", `{"method":"eth_getBlockByHash","params":["0x123",true],"jsonrpc":"2.0","id":2}`)
|
|
l2Cache.Set(key, []byte(`{"result":{"number":"0x123"},"id":2}`), models.TTL{Fresh: time.Hour, Stale: time.Minute})
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
expectedFound: true,
|
|
},
|
|
{
|
|
name: "invalid request - missing chain",
|
|
requestBody: CacheRequest{
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_blockNumber"}`,
|
|
},
|
|
expectedStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "invalid request - empty raw body",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: "",
|
|
},
|
|
expectedStatus: http.StatusBadRequest,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Reset caches
|
|
l1Cache.data = make(map[string]*models.CacheEntry)
|
|
l2Cache.data = make(map[string]*models.CacheEntry)
|
|
|
|
if tt.setupCache != nil {
|
|
tt.setupCache()
|
|
}
|
|
|
|
body, _ := json.Marshal(tt.requestBody)
|
|
req := httptest.NewRequest("POST", "/cache/get", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
w := httptest.NewRecorder()
|
|
server.handleGet(w, req)
|
|
|
|
if w.Code != tt.expectedStatus {
|
|
t.Errorf("handleGet() status = %v, want %v", w.Code, tt.expectedStatus)
|
|
}
|
|
|
|
if tt.expectedStatus == http.StatusOK {
|
|
var response CacheResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("Failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
if response.Found != tt.expectedFound {
|
|
t.Errorf("handleGet() Found = %v, want %v", response.Found, tt.expectedFound)
|
|
}
|
|
|
|
if !response.Success {
|
|
t.Errorf("handleGet() Success = false, want true")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServer_HandleGet_CacheStatus(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
logger := zaptest.NewLogger(t)
|
|
cacheService, l1Cache, l2Cache := setupCacheService(ctrl, logger)
|
|
|
|
server := NewServer(cacheService, logger)
|
|
|
|
tests := []struct {
|
|
name string
|
|
requestBody CacheRequest
|
|
setupCache func()
|
|
expectedStatus int
|
|
expectedCacheStatus models.CacheStatus
|
|
}{
|
|
{
|
|
name: "cache status MISS",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_blockNumber","params":[],"jsonrpc":"2.0","id":1}`,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
expectedCacheStatus: models.CacheStatusMiss,
|
|
},
|
|
{
|
|
name: "cache status HIT",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_blockNumber","params":[],"jsonrpc":"2.0","id":1}`,
|
|
},
|
|
setupCache: func() {
|
|
key, _ := generateCacheKey("ethereum", "mainnet", `{"method":"eth_blockNumber","params":[],"jsonrpc":"2.0","id":1}`)
|
|
l1Cache.Set(key, []byte(`{"result":"0x123","id":1}`), models.TTL{Fresh: time.Hour, Stale: time.Minute})
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
expectedCacheStatus: models.CacheStatusHit,
|
|
},
|
|
{
|
|
name: "cache status BYPASS",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"unknown_method","params":[],"jsonrpc":"2.0","id":1}`,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
expectedCacheStatus: models.CacheStatusBypass,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Reset caches
|
|
l1Cache.data = make(map[string]*models.CacheEntry)
|
|
l2Cache.data = make(map[string]*models.CacheEntry)
|
|
|
|
if tt.setupCache != nil {
|
|
tt.setupCache()
|
|
}
|
|
|
|
body, _ := json.Marshal(tt.requestBody)
|
|
req := httptest.NewRequest("POST", "/cache/get", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
w := httptest.NewRecorder()
|
|
server.handleGet(w, req)
|
|
|
|
if w.Code != tt.expectedStatus {
|
|
t.Errorf("handleGet() status = %v, want %v", w.Code, tt.expectedStatus)
|
|
}
|
|
|
|
if tt.expectedStatus == http.StatusOK {
|
|
var response CacheResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("Failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
if response.CacheStatus != tt.expectedCacheStatus {
|
|
t.Errorf("handleGet() CacheStatus = %v, want %v", response.CacheStatus, tt.expectedCacheStatus)
|
|
}
|
|
|
|
if !response.Success {
|
|
t.Errorf("handleGet() Success = false, want true")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServer_HandleSet(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
logger := zaptest.NewLogger(t)
|
|
cacheService, l1Cache, l2Cache := setupCacheService(ctrl, logger)
|
|
|
|
server := NewServer(cacheService, logger)
|
|
|
|
tests := []struct {
|
|
name string
|
|
requestBody CacheRequest
|
|
expectedStatus int
|
|
checkCache bool
|
|
}{
|
|
{
|
|
name: "successful set",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_getBlockByHash","params":["0x123",true],"jsonrpc":"2.0","id":1}`,
|
|
Data: `{"result":{"number":"0x123"},"id":1}`,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
checkCache: true,
|
|
},
|
|
{
|
|
name: "set with custom TTL",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_blockNumber","params":[],"jsonrpc":"2.0","id":2}`,
|
|
Data: `{"result":"0x456","id":2}`,
|
|
TTL: intPtr(3600),
|
|
StaleTTL: intPtr(360),
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
checkCache: true,
|
|
},
|
|
{
|
|
name: "invalid request - missing data",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_blockNumber"}`,
|
|
},
|
|
expectedStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "zero TTL method",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"unknown_method"}`,
|
|
Data: `{"result":"test"}`,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
checkCache: false, // Should not be cached due to zero TTL
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Reset caches
|
|
l1Cache.data = make(map[string]*models.CacheEntry)
|
|
l2Cache.data = make(map[string]*models.CacheEntry)
|
|
|
|
body, _ := json.Marshal(tt.requestBody)
|
|
req := httptest.NewRequest("POST", "/cache/set", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
w := httptest.NewRecorder()
|
|
server.handleSet(w, req)
|
|
|
|
if w.Code != tt.expectedStatus {
|
|
t.Errorf("handleSet() status = %v, want %v", w.Code, tt.expectedStatus)
|
|
}
|
|
|
|
if tt.expectedStatus == http.StatusOK {
|
|
var response CacheResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("Failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
if !response.Success {
|
|
t.Errorf("handleSet() Success = false, want true")
|
|
}
|
|
|
|
// Check if data was actually cached
|
|
if tt.checkCache {
|
|
key, err := generateCacheKey(tt.requestBody.Chain, tt.requestBody.Network, tt.requestBody.RawBody)
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate cache key: %v", err)
|
|
}
|
|
if _, found := l1Cache.data[key]; !found {
|
|
t.Errorf("handleSet() data not found in L1 cache")
|
|
}
|
|
if _, found := l2Cache.data[key]; !found {
|
|
t.Errorf("handleSet() data not found in L2 cache")
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestServer_HandleSet_SkipNullCache verifies that null results for skip-listed methods
|
|
// are not written to L1 or L2 cache
|
|
func TestServer_HandleSet_SkipNullCache(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
logger := zaptest.NewLogger(t)
|
|
cacheService, l1Cache, l2Cache := setupCacheService(ctrl, logger)
|
|
|
|
server := NewServer(cacheService, logger)
|
|
|
|
tests := []struct {
|
|
name string
|
|
requestBody CacheRequest
|
|
expectedStatus int
|
|
expectCached bool
|
|
description string
|
|
}{
|
|
{
|
|
name: "skip-listed method with null result should not cache",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_getTransactionReceipt","params":["0x123"],"jsonrpc":"2.0","id":1}`,
|
|
Data: `{"jsonrpc":"2.0","id":1,"result":null}`,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
expectCached: false,
|
|
description: "eth_getTransactionReceipt with null result should skip caching",
|
|
},
|
|
{
|
|
name: "skip-listed method with non-null result should cache",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_getBlockByHash","params":["0x456",true],"jsonrpc":"2.0","id":2}`,
|
|
Data: `{"jsonrpc":"2.0","id":2,"result":{"number":"0x123","hash":"0x456"}}`,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
expectCached: true,
|
|
description: "eth_getBlockByHash with non-null result should be cached",
|
|
},
|
|
{
|
|
name: "non-skip-listed method with null result should cache",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_getBalance","params":["0x789","latest"],"jsonrpc":"2.0","id":3}`,
|
|
Data: `{"jsonrpc":"2.0","id":3,"result":null}`,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
expectCached: false, // eth_getBalance has TTL 0 in mock, so won't cache anyway
|
|
description: "non-skip-listed method returns based on TTL rules",
|
|
},
|
|
{
|
|
name: "eth_getBlockByHash with null result should not cache",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_getBlockByHash","params":["0xnonexistent",true],"jsonrpc":"2.0","id":4}`,
|
|
Data: `{"jsonrpc":"2.0","id":4,"result":null}`,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
expectCached: false,
|
|
description: "eth_getBlockByHash with null result should skip caching",
|
|
},
|
|
{
|
|
name: "eth_getBlockByNumber with null result should not cache",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_getBlockByNumber","params":["0x999999999",true],"jsonrpc":"2.0","id":5}`,
|
|
Data: `{"jsonrpc":"2.0","id":5,"result":null}`,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
expectCached: false,
|
|
description: "eth_getBlockByNumber with null result should skip caching",
|
|
},
|
|
{
|
|
name: "eth_getTransactionByHash with null result should not cache",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_getTransactionByHash","params":["0xpending"],"jsonrpc":"2.0","id":6}`,
|
|
Data: `{"jsonrpc":"2.0","id":6,"result":null}`,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
expectCached: false,
|
|
description: "eth_getTransactionByHash with null result should skip caching",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Reset caches before each test
|
|
l1Cache.data = make(map[string]*models.CacheEntry)
|
|
l2Cache.data = make(map[string]*models.CacheEntry)
|
|
|
|
body, _ := json.Marshal(tt.requestBody)
|
|
req := httptest.NewRequest("POST", "/cache/set", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
w := httptest.NewRecorder()
|
|
server.handleSet(w, req)
|
|
|
|
if w.Code != tt.expectedStatus {
|
|
t.Errorf("%s: status = %v, want %v", tt.description, w.Code, tt.expectedStatus)
|
|
}
|
|
|
|
if tt.expectedStatus == http.StatusOK {
|
|
var response CacheResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("Failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
if !response.Success {
|
|
t.Errorf("%s: Success = false, want true", tt.description)
|
|
}
|
|
|
|
// Generate cache key to check if data was cached
|
|
key, err := generateCacheKey(tt.requestBody.Chain, tt.requestBody.Network, tt.requestBody.RawBody)
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate cache key: %v", err)
|
|
}
|
|
|
|
_, foundInL1 := l1Cache.data[key]
|
|
_, foundInL2 := l2Cache.data[key]
|
|
|
|
if tt.expectCached {
|
|
if !foundInL1 {
|
|
t.Errorf("%s: expected data in L1 cache, but not found", tt.description)
|
|
}
|
|
if !foundInL2 {
|
|
t.Errorf("%s: expected data in L2 cache, but not found", tt.description)
|
|
}
|
|
} else {
|
|
if foundInL1 {
|
|
t.Errorf("%s: expected L1 cache to be empty, but found data", tt.description)
|
|
}
|
|
if foundInL2 {
|
|
t.Errorf("%s: expected L2 cache to be empty, but found data", tt.description)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServer_HandleCacheInfo(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
logger := zaptest.NewLogger(t)
|
|
cacheService, _, _ := setupCacheService(ctrl, logger)
|
|
|
|
server := NewServer(cacheService, logger)
|
|
|
|
tests := []struct {
|
|
name string
|
|
requestBody CacheRequest
|
|
expectedStatus int
|
|
expectedTTL int
|
|
}{
|
|
{
|
|
name: "permanent method",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_getBlockByHash","params":["0x123",true],"jsonrpc":"2.0","id":1}`,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
expectedTTL: 86400,
|
|
},
|
|
{
|
|
name: "short method",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: `{"method":"eth_blockNumber","params":[],"jsonrpc":"2.0","id":2}`,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
expectedTTL: 5,
|
|
},
|
|
{
|
|
name: "invalid request",
|
|
requestBody: CacheRequest{
|
|
Chain: "ethereum",
|
|
Network: "mainnet",
|
|
RawBody: "",
|
|
},
|
|
expectedStatus: http.StatusBadRequest,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
body, _ := json.Marshal(tt.requestBody)
|
|
req := httptest.NewRequest("POST", "/cache/info", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
w := httptest.NewRecorder()
|
|
server.handleCacheInfo(w, req)
|
|
|
|
if w.Code != tt.expectedStatus {
|
|
t.Errorf("handleCacheInfo() status = %v, want %v", w.Code, tt.expectedStatus)
|
|
}
|
|
|
|
if tt.expectedStatus == http.StatusOK {
|
|
var response CacheResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("Failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
if !response.Success {
|
|
t.Errorf("handleCacheInfo() Success = false, want true")
|
|
}
|
|
|
|
if response.TTL != tt.expectedTTL {
|
|
t.Errorf("handleCacheInfo() TTL = %v, want %v", response.TTL, tt.expectedTTL)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServer_HandleHealth(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
logger := zaptest.NewLogger(t)
|
|
cacheService, _, _ := setupCacheService(ctrl, logger)
|
|
|
|
server := NewServer(cacheService, logger)
|
|
|
|
req := httptest.NewRequest("GET", "/health", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
server.handleHealth(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("handleHealth() status = %v, want %v", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var response map[string]interface{}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("Failed to unmarshal health response: %v", err)
|
|
}
|
|
|
|
if status, ok := response["status"]; !ok || status != "healthy" {
|
|
t.Errorf("handleHealth() status = %v, want 'healthy'", status)
|
|
}
|
|
}
|
|
|
|
// Helper function to create int pointer
|
|
func intPtr(i int) *int {
|
|
return &i
|
|
}
|