- Updated branding from "SolaceScanScout" to "Solace" across various files including deployment scripts, API responses, and documentation. - Changed default base URL for Playwright tests and updated security headers to reflect the new branding. - Enhanced README and API documentation to include new authentication endpoints and product access details. This refactor aligns the project branding and improves clarity in the API documentation.
375 lines
11 KiB
Go
375 lines
11 KiB
Go
package rest_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/explorer/backend/api/rest"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// setupTestServer creates a test server with a test database
|
|
func setupTestServer(t *testing.T) (*rest.Server, *http.ServeMux) {
|
|
// Use test database or in-memory database
|
|
// For now, we'll use a mock approach
|
|
db, err := setupTestDB(t)
|
|
if err != nil {
|
|
t.Skipf("Skipping test: database not available: %v", err)
|
|
return nil, nil
|
|
}
|
|
|
|
server := rest.NewServer(db, 138) // ChainID 138
|
|
mux := http.NewServeMux()
|
|
server.SetupRoutes(mux)
|
|
|
|
return server, mux
|
|
}
|
|
|
|
// setupTestDB creates a test database connection. Returns (nil, nil) so unit tests
|
|
// run without a real DB; handlers use requireDB(w) and return 503 when db is nil.
|
|
// For integration tests with a DB, replace this with a real connection (e.g. testcontainers).
|
|
func setupTestDB(t *testing.T) (*pgxpool.Pool, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
// TestHealthEndpoint tests the health check endpoint
|
|
func TestHealthEndpoint(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
if mux == nil {
|
|
t.Skip("setupTestServer skipped (no DB)")
|
|
return
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/health", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
// Without DB we get 503 degraded; with DB we get 200
|
|
assert.True(t, w.Code == http.StatusOK || w.Code == http.StatusServiceUnavailable, "code=%d", w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
status, _ := response["status"].(string)
|
|
assert.True(t, status == "healthy" || status == "degraded", "status=%s", status)
|
|
}
|
|
|
|
// TestListBlocks tests the blocks list endpoint
|
|
func TestListBlocks(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/blocks?limit=10&page=1", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
// Without DB returns 503; with DB returns 200 or 500
|
|
assert.True(t, w.Code == http.StatusOK || w.Code == http.StatusInternalServerError || w.Code == http.StatusServiceUnavailable, "code=%d", w.Code)
|
|
}
|
|
|
|
// TestGetBlockByNumber tests getting a block by number
|
|
func TestGetBlockByNumber(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/blocks/138/1000", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
// Without DB returns 503; with DB returns 200, 404, or 500
|
|
assert.True(t, w.Code == http.StatusOK || w.Code == http.StatusNotFound || w.Code == http.StatusInternalServerError || w.Code == http.StatusServiceUnavailable, "code=%d", w.Code)
|
|
}
|
|
|
|
// TestListTransactions tests the transactions list endpoint
|
|
func TestListTransactions(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/transactions?limit=10&page=1", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.True(t, w.Code == http.StatusOK || w.Code == http.StatusInternalServerError || w.Code == http.StatusServiceUnavailable, "code=%d", w.Code)
|
|
}
|
|
|
|
// TestGetTransactionByHash tests getting a transaction by hash
|
|
func TestGetTransactionByHash(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/transactions/138/0x1234567890abcdef", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.True(t, w.Code == http.StatusOK || w.Code == http.StatusNotFound || w.Code == http.StatusInternalServerError || w.Code == http.StatusServiceUnavailable, "code=%d", w.Code)
|
|
}
|
|
|
|
// TestSearchEndpoint tests the unified search endpoint
|
|
func TestSearchEndpoint(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
query string
|
|
wantCode int
|
|
}{
|
|
{"block number", "?q=1000", http.StatusOK},
|
|
{"address", "?q=0x1234567890abcdef1234567890abcdef12345678", http.StatusOK},
|
|
{"transaction hash", "?q=0xabcdef1234567890abcdef1234567890abcdef12", http.StatusOK},
|
|
{"empty query", "?q=", http.StatusBadRequest},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/v1/search"+tc.query, nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.True(t, w.Code == tc.wantCode || w.Code == http.StatusInternalServerError || w.Code == http.StatusServiceUnavailable, "code=%d", w.Code)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestTrack1Endpoints tests Track 1 (public) endpoints
|
|
func TestTrack1Endpoints(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
endpoint string
|
|
method string
|
|
}{
|
|
{"latest blocks", "/api/v1/track1/blocks/latest", "GET"},
|
|
{"latest transactions", "/api/v1/track1/txs/latest", "GET"},
|
|
{"bridge status", "/api/v1/track1/bridge/status", "GET"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(tc.method, tc.endpoint, nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
// Track 1 routes not registered in test mux (only SetupRoutes), so 404 is ok; with full setup 200/500
|
|
assert.True(t, w.Code == http.StatusOK || w.Code == http.StatusNotFound || w.Code == http.StatusInternalServerError, "code=%d", w.Code)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCORSHeaders tests CORS headers are present
|
|
func TestCORSHeaders(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/health", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
// Check for CORS headers (if implemented)
|
|
// This is a placeholder - actual implementation may vary
|
|
assert.NotNil(t, w.Header())
|
|
}
|
|
|
|
// TestErrorHandling tests error responses
|
|
func TestErrorHandling(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
// Test invalid block number
|
|
req := httptest.NewRequest("GET", "/api/v1/blocks/138/invalid", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.True(t, w.Code >= http.StatusBadRequest)
|
|
|
|
var errorResponse map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &errorResponse)
|
|
if err == nil {
|
|
assert.NotNil(t, errorResponse["error"])
|
|
}
|
|
}
|
|
|
|
// TestPagination tests pagination parameters
|
|
func TestPagination(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
query string
|
|
wantCode int
|
|
}{
|
|
{"valid pagination", "?limit=10&page=1", http.StatusOK},
|
|
{"large limit", "?limit=1000&page=1", http.StatusOK}, // Should be capped
|
|
{"invalid page", "?limit=10&page=0", http.StatusBadRequest},
|
|
{"negative limit", "?limit=-10&page=1", http.StatusBadRequest},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/v1/blocks"+tc.query, nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.True(t, w.Code == tc.wantCode || w.Code == http.StatusInternalServerError || w.Code == http.StatusServiceUnavailable, "code=%d", w.Code)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthNonceRequiresDB(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("POST", "/api/v1/auth/nonce", bytes.NewBufferString(`{"address":"0x4A666F96fC8764181194447A7dFdb7d471b301C8"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, response["error"])
|
|
}
|
|
|
|
func TestAuthWalletRequiresDB(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("POST", "/api/v1/auth/wallet", bytes.NewBufferString(`{"address":"0x4A666F96fC8764181194447A7dFdb7d471b301C8","signature":"0xdeadbeef","nonce":"abc"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, response["error"])
|
|
}
|
|
|
|
func TestAccessProductsEndpoint(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/access/products", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, response["products"])
|
|
}
|
|
|
|
func TestAccessMeRequiresUserSession(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/access/me", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, response["error"])
|
|
}
|
|
|
|
func TestAccessSubscriptionsRequiresUserSession(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/access/subscriptions", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
|
|
}
|
|
|
|
func TestAccessUsageRequiresUserSession(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/access/usage", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
|
|
}
|
|
|
|
func TestAccessAuditRequiresUserSession(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/access/audit", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
|
|
}
|
|
|
|
func TestAccessAdminAuditRequiresUserSession(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/access/admin/audit", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
|
|
}
|
|
|
|
func TestAccessInternalValidateKeyRequiresDB(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/access/internal/validate-key", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
|
|
}
|
|
|
|
func TestAIContextEndpoint(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/ai/context?q=cUSDT+bridge", nil)
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, response["context"])
|
|
}
|
|
|
|
func TestAIChatEndpointRequiresOpenAIKey(t *testing.T) {
|
|
_, mux := setupTestServer(t)
|
|
|
|
body := bytes.NewBufferString(`{"messages":[{"role":"user","content":"What is live on Chain 138?"}]}`)
|
|
req := httptest.NewRequest("POST", "/api/v1/ai/chat", body)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
|
|
}
|
|
|
|
// TestRequestTimeout tests request timeout handling
|
|
func TestRequestTimeout(t *testing.T) {
|
|
// This would test timeout behavior
|
|
// Implementation depends on timeout middleware
|
|
t.Skip("Requires timeout middleware implementation")
|
|
}
|
|
|
|
// BenchmarkListBlocks benchmarks the blocks list endpoint
|
|
func BenchmarkListBlocks(b *testing.B) {
|
|
_, mux := setupTestServer(&testing.T{})
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/blocks?limit=10&page=1", nil)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
}
|
|
}
|