Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 25 additions & 22 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,32 +300,35 @@ func NewApiServer(config config.Config) *ApiServer {
if app.rateLimitMiddleware != nil {
app.Use(app.rateLimitMiddleware.Middleware(app))
}
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))
}
// 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