Skip to content
Open
53 changes: 25 additions & 28 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import (
"github.com/mcuadros/go-defaults"
"github.com/segmentio/encoding/json"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

//go:embed swagger/swagger-v1.yaml
Expand Down Expand Up @@ -302,37 +301,35 @@ func NewApiServer(config config.Config) *ApiServer {
if app.rateLimitMiddleware != nil {
app.Use(app.rateLimitMiddleware.Middleware(app))
}
// In test, only log request errors to avoid noisy success logs
requestLogger := logger
if config.Env == "test" {
requestLogger = logger.WithOptions(zap.IncreaseLevel(zapcore.ErrorLevel))
}
app.Use(fiberzap.New(fiberzap.Config{
Logger: requestLogger,
FieldsFunc: func(c *fiber.Ctx) []zap.Field {
fields := []zap.Field{}

if startTime, ok := c.Locals("start").(time.Time); ok {
latencyMs := float64(time.Since(startTime).Nanoseconds()) / float64(time.Millisecond)
fields = append(fields, zap.Float64("latency_ms", latencyMs))
}
// Avoid request log spam in tests; test failures still include assertion output.
if config.Env != "test" {
app.Use(fiberzap.New(fiberzap.Config{
Logger: logger,
FieldsFunc: func(c *fiber.Ctx) []zap.Field {
fields := []zap.Field{}

if startTime, ok := c.Locals("start").(time.Time); ok {
latencyMs := float64(time.Since(startTime).Nanoseconds()) / float64(time.Millisecond)
fields = append(fields, zap.Float64("latency_ms", latencyMs))
}

// Add upstream server to logs, if found
if upstream, ok := c.Locals("upstream").(string); ok && upstream != "" {
fields = append(fields, zap.String("upstream", upstream))
}
// Add upstream server to logs, if found
if upstream, ok := c.Locals("upstream").(string); ok && upstream != "" {
fields = append(fields, zap.String("upstream", upstream))
}

if requestId, ok := c.Locals("requestId").(string); ok && requestId != "" {
fields = append(fields, zap.String("request_id", requestId))
}
if requestId, ok := c.Locals("requestId").(string); ok && requestId != "" {
fields = append(fields, zap.String("request_id", requestId))
}

ipAddress := apiutils.GetIP(c)
fields = append(fields, zap.String("ip", ipAddress))
ipAddress := apiutils.GetIP(c)
fields = append(fields, zap.String("ip", ipAddress))

return fields
},
Fields: []string{"status", "method", "url", "route"},
}))
return fields
},
Fields: []string{"status", "method", "url", "route"},
}))
}

app.Get("/", app.home)

Expand Down
1 change: 1 addition & 0 deletions api/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func emptyTestApp(t *testing.T) *ApiServer {

app := NewApiServer(config.Config{
Env: "test",
LogLevel: "fatal",
ReadDbUrl: pool.Config().ConnString(),
WriteDbUrl: pool.Config().ConnString(),
RunMigrations: false,
Expand Down
70 changes: 70 additions & 0 deletions api/v1_challenges_info_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package api

import (
"testing"
"time"

"api.audius.co/database"
"github.com/stretchr/testify/assert"
)

func TestV1ChallengesInfo(t *testing.T) {
app := emptyTestApp(t)

now := time.Now().UTC()

fixtures := database.FixtureMap{
"challenges": {
{
"id": "challenge-aggregate",
"type": "aggregate",
"amount": "100000000",
"active": true,
"step_count": 10,
"starting_block": 100,
"weekly_pool": 100,
"cooldown_days": 7,
},
},
"challenge_disbursements": {
{
"challenge_id": "challenge-aggregate",
"user_id": 1,
"specifier": "spec-a",
"signature": "sig-a",
"slot": 1,
"amount": "200000000",
"created_at": now,
},
},
}

database.Seed(app.pool.Replicas[0], fixtures)

status, body := testGet(t, app, "/v1/challenges/challenge-aggregate/info")
assert.Equal(t, 200, status)
jsonAssert(t, body, map[string]any{
"data.challenge_id": "challenge-aggregate",
"data.type": "aggregate",
"data.amount": "100000000",
"data.weekly_pool": 100,
"data.weekly_pool_remaining": 98,
})

status, body = testGet(t, app, "/v1/challenges/challenge-aggregate/info?weekly_pool_min_amount=99")
assert.Equal(t, 500, status)
jsonAssert(t, body, map[string]any{
"data.challenge_id": "challenge-aggregate",
"data.weekly_pool_remaining": 98,
})
}

func TestV1ChallengesInfoInvalidWeeklyPoolMinAmount(t *testing.T) {
app := emptyTestApp(t)

status, body := testGet(t, app, "/v1/challenges/any/info?weekly_pool_min_amount=-1")
assert.Equal(t, 400, status)
jsonAssert(t, body, map[string]any{
"error": "weekly_pool_min_amount is invalid",
})
}
21 changes: 15 additions & 6 deletions api/v1_claim_rewards_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"net/http"
"net/http/httptest"
"sync"
"testing"

"api.audius.co/config"
Expand All @@ -14,11 +15,17 @@ import (

func TestFetchAttestations(t *testing.T) {
// Track which URLs are called
var urlCallCountMu sync.Mutex
urlCallCount := make(map[string]int)
incrementURLCallCount := func(host string) {
urlCallCountMu.Lock()
defer urlCallCountMu.Unlock()
urlCallCount[host]++
}

// Create mock HTTP servers for AAO and validators
aaoServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
urlCallCount[r.Host]++
incrementURLCallCount(r.Host)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
w.Write([]byte(`{"result": "aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd"}`))
Expand All @@ -27,15 +34,15 @@ func TestFetchAttestations(t *testing.T) {

// Create separate validator servers
validator1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
urlCallCount[r.Host]++
incrementURLCallCount(r.Host)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
w.Write([]byte(`{"attestation": "aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd", "owner": "0x1111111111111111111111111111111111111111"}`))
}))
defer validator1.Close()

validator2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
urlCallCount[r.Host]++
incrementURLCallCount(r.Host)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
// Duplicate owner should not be selected
Expand All @@ -44,23 +51,23 @@ func TestFetchAttestations(t *testing.T) {
defer validator2.Close()

validator3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
urlCallCount[r.Host]++
incrementURLCallCount(r.Host)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
w.Write([]byte(`{"attestation": "aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd", "owner": "0x3333333333333333333333333333333333333333"}`))
}))
defer validator3.Close()

validator4 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
urlCallCount[r.Host]++
incrementURLCallCount(r.Host)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(400)
w.Write([]byte(`{"error": "unhappy validator"}`))
}))
defer validator4.Close()

validator5 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
urlCallCount[r.Host]++
incrementURLCallCount(r.Host)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
w.Write([]byte(`{"attestation": "aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd", "owner": "0x5555555555555555555555555555555555555555"}`))
Expand Down Expand Up @@ -127,6 +134,8 @@ func TestFetchAttestations(t *testing.T) {
assert.False(t, addresses["0x4444444444444444444444444444444444444444"], "validator4 should not be present in attestations")

// Verify no URL was called more than once
urlCallCountMu.Lock()
defer urlCallCountMu.Unlock()
for url, count := range urlCallCount {
assert.LessOrEqual(t, count, 1, "URL %s should never be called more than once, but was called %d times", url, count)
}
Expand Down
94 changes: 94 additions & 0 deletions api/v1_coin_members_count_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package api

import (
"testing"

"api.audius.co/database"
"github.com/stretchr/testify/assert"
)

func TestV1CoinMembersCount(t *testing.T) {
app := emptyTestApp(t)

fixtures := database.FixtureMap{
"artist_coins": {
{
"mint": "coin-mint-1",
"ticker": "COINONE",
"user_id": 1,
"decimals": 2,
},
},
"sol_user_balances": {
{
"user_id": 1,
"mint": "coin-mint-1",
"balance": 99,
"created_at": parseTimeWithLayout(
t,
"2024-01-01 00:00:00",
"2006-01-02 15:04:05",
),
"updated_at": parseTimeWithLayout(
t,
"2024-01-01 00:00:00",
"2006-01-02 15:04:05",
),
},
{
"user_id": 2,
"mint": "coin-mint-1",
"balance": 100,
"created_at": parseTimeWithLayout(
t,
"2024-01-01 00:00:00",
"2006-01-02 15:04:05",
),
"updated_at": parseTimeWithLayout(
t,
"2024-01-01 00:00:00",
"2006-01-02 15:04:05",
),
},
{
"user_id": 3,
"mint": "coin-mint-1",
"balance": 250,
"created_at": parseTimeWithLayout(
t,
"2024-01-01 00:00:00",
"2006-01-02 15:04:05",
),
"updated_at": parseTimeWithLayout(
t,
"2024-01-01 00:00:00",
"2006-01-02 15:04:05",
),
},
},
}

database.Seed(app.pool.Replicas[0], fixtures)

status, body := testGet(t, app, "/v1/coins/coin-mint-1/members/count")
assert.Equal(t, 200, status)
jsonAssert(t, body, map[string]any{
"data": 2,
})

status, body = testGet(t, app, "/v1/coins/coin-mint-1/members/count?min_balance=2.5")
assert.Equal(t, 200, status)
jsonAssert(t, body, map[string]any{
"data": 1,
})
}

func TestV1CoinMembersCountInvalidMinBalance(t *testing.T) {
app := emptyTestApp(t)

status, body := testGet(t, app, "/v1/coins/coin-mint-1/members/count?min_balance=-1")
assert.Equal(t, 400, status)
jsonAssert(t, body, map[string]any{
"error": "min_balance is invalid",
})
}
60 changes: 60 additions & 0 deletions api/v1_coins_volume_leaders_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package api

import (
"fmt"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
)

func TestV1CoinsVolumeLeaders(t *testing.T) {
app := emptyTestApp(t)

status, body := testGet(t, app, "/v1/coins/volume-leaders?from=not-a-date")
assert.Equal(t, 400, status)
jsonAssert(t, body, map[string]any{
"error": "from is invalid",
})

now := time.Now().UTC().Truncate(time.Second)
from := now.Add(-2 * time.Hour).Format(time.RFC3339)
to := now.Add(-1 * time.Hour).Format(time.RFC3339)

status, body = testGet(
t, app,
fmt.Sprintf("/v1/coins/volume-leaders?from=%s&to=%s&limit=5&offset=0", from, to),
)
assert.Equal(t, 200, status)
assert.True(t, gjson.GetBytes(body, "data").IsArray())

status, body = testGet(
t, app,
fmt.Sprintf("/v1/coins/volume-leaders?from=%s&to=%s", now.Format(time.RFC3339), now.Add(-1*time.Hour).Format(time.RFC3339)),
)
assert.Equal(t, 400, status)
jsonAssert(t, body, map[string]any{
"error": "To date must be after from date",
})

status, body = testGet(
t, app,
fmt.Sprintf("/v1/coins/volume-leaders?from=%s&to=%s", now.Add(-9*24*time.Hour).Format(time.RFC3339), now.Format(time.RFC3339)),
)
assert.Equal(t, 400, status)
jsonAssert(t, body, map[string]any{
"error": "Time range must be <= 7 days",
})

tooOldFrom := now.Add(-12 * 24 * time.Hour)
tooOldTo := tooOldFrom.Add(6 * time.Hour)
status, body = testGet(
t, app,
fmt.Sprintf("/v1/coins/volume-leaders?from=%s&to=%s", tooOldFrom.Format(time.RFC3339), tooOldTo.Format(time.RFC3339)),
)
assert.Equal(t, 400, status)
jsonAssert(t, body, map[string]any{
"error": "Time range too old",
})
}
Loading
Loading