Skip to content

Commit

Permalink
Merge branch 'BEDS-1068/fix-summary-ignored-shared-groups' into stagi…
Browse files Browse the repository at this point in the history
…ng-2
  • Loading branch information
remoterami committed Jan 23, 2025
2 parents 0ed1e97 + 86ac7a7 commit 90977d8
Show file tree
Hide file tree
Showing 2 changed files with 250 additions and 33 deletions.
250 changes: 228 additions & 22 deletions backend/pkg/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"testing"
"time"

"github.com/doug-martin/goqu/v9"
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
"github.com/gavv/httpexpect/v2"
"github.com/go-openapi/spec"
Expand Down Expand Up @@ -92,26 +93,29 @@ func setup() error {
return fmt.Errorf("error running migrations: %w", err)
}

// insert dummy user for testing (email: admin@admin, password: admin)
pHash, _ := bcrypt.GenerateFromPassword([]byte("admin"), 10)
_, err = tempDb.Exec(`
INSERT INTO users (password, email, register_ts, api_key, email_confirmed)
VALUES ($1, $2, TO_TIMESTAMP($3), $4, $5)`,
string(pHash), "admin@admin.com", time.Now().Unix(), "admin", true,
)
if err != nil {
return fmt.Errorf("error inserting user: %w", err)
}
for _, user := range testUsers {
pHash, _ := bcrypt.GenerateFromPassword([]byte(user.Password), 10)
insertDs := goqu.Dialect("postgres").
Insert("users").
Rows(struct {
User
RegisterTs time.Time `db:"register_ts"`
PasswordHash string `db:"password"`
}{
user,
time.Now(),
string(pHash),
})

// required for shared dashboard
pHash, _ = bcrypt.GenerateFromPassword([]byte("admin"), 10)
_, err = tempDb.Exec(`
INSERT INTO users (id, password, email, register_ts, api_key, email_confirmed)
VALUES ($1, $2, $3, TO_TIMESTAMP($4), $5, $6)`,
122558, string(pHash), "admin2@admin.com", time.Now().Unix(), "admin2", true,
)
if err != nil {
return fmt.Errorf("error inserting user 2: %w", err)
query, args, err := insertDs.Prepared(true).ToSQL()
if err != nil {
return fmt.Errorf("error preparing query: %w", err)
}

_, err = tempDb.Exec(query, args...)
if err != nil {
return err
}
}

// insert dummy api weight for testing
Expand Down Expand Up @@ -187,10 +191,13 @@ func getExpectConfig(t *testing.T, ts *httptest.Server) httpexpect.Config {
}
}

func login(e *httpexpect.Expect) {
func login(e *httpexpect.Expect, user *User) {
if user == nil {
return
}
e.POST("/api/i/login").
WithHeader("Content-Type", "application/json").
WithJSON(map[string]interface{}{"email": "admin@admin.com", "password": "admin"}).
WithJSON(map[string]interface{}{"email": user.Email, "password": user.Password}).
Expect().
Status(http.StatusOK)
}
Expand Down Expand Up @@ -247,7 +254,7 @@ func TestInternalLoginHandler(t *testing.T) {
})

t.Run("login with correct user and password", func(t *testing.T) {
login(e)
login(e, &testUsers[0])
})

t.Run("check if user is logged in and has a valid session", func(t *testing.T) {
Expand Down Expand Up @@ -493,6 +500,205 @@ func TestPublicAndSharedDashboards(t *testing.T) {
}
}

type User struct {
Email string `db:"email"`
Password string
ApiKey string `db:"api_key"`
// optional
Id uint `db:"id" goqu:"omitempty"`
UserGroup string `db:"user_group" goqu:"omitempty"`
EmailConfirmed bool `db:"email_confirmed" goqu:"omitempty"`
}

var testUsers = []User{
{Email: "admin@admin.com", Password: "admin", ApiKey: "admin", UserGroup: api_types.UserGroupAdmin, EmailConfirmed: true},
// holesky
{Id: 122558, Email: "admin2@admin.com", Password: "admin", ApiKey: "admin2", UserGroup: api_types.UserGroupAdmin, EmailConfirmed: true},
{Id: 14, Email: "default@admin.com", Password: "default", ApiKey: "default", EmailConfirmed: true},
{Id: 113321, Email: "admin3@admin.com", Password: "admin", ApiKey: "admin3", UserGroup: api_types.UserGroupAdmin, EmailConfirmed: true},
{Id: 3, Email: "admin4@admin.com", Password: "admin", ApiKey: "admin4", UserGroup: api_types.UserGroupAdmin, EmailConfirmed: true},
}

func TestSummaryChartDetailed(t *testing.T) {
e := httpexpect.WithConfig(getExpectConfig(t, ts))

type TestConfig struct {
Dashboard string
User *User
}
// holesky
cases := []TestConfig{
// anonymous
{Dashboard: "MSwxNTU2MSwxNTY"},
// primary
{Dashboard: "v-009b2943-3268-44f7-a137-2878fc73268b"}, // not shared groups
{Dashboard: "15", User: &testUsers[2]},
// RP
{Dashboard: "v-80d7edaa-74fb-4129-a41e-7700756961cf"}, // shared groups
{Dashboard: "5090", User: &testUsers[1]},
// other
{Dashboard: "5113", User: &testUsers[3]},
// megatron
{Dashboard: "5001", User: &testUsers[4]},
}

baseUrl := "/api/i/validator-dashboards/{id}/summary-chart"

// anonymous
t.Run("anon", func(t *testing.T) {
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
e.GET(baseUrl, cases[0].Dashboard).
WithQuery("group_ids", "-1").
WithQuery("aggregation", "hourly").
Expect().Status(http.StatusOK).JSON().Decode(&resp)

assert.Greater(t, len(resp.Data.Categories), 0, "summary chart categories should not be empty")
require.Greater(t, len(resp.Data.Series), 0, "summary chart series should not be empty")
assert.Equal(t, 1, len(resp.Data.Series), "summary chart series should only contain one group")
assert.Equal(t, api_types.AllGroups, resp.Data.Series[0].Id, "summary chart series should contain default group id")
})

t.Run("anon: conflict aggregation", func(t *testing.T) {
e.GET(baseUrl, cases[0].Dashboard).
WithQuery("group_ids", "-1").
WithQuery("aggregation", "daily").
Expect().Status(http.StatusConflict).JSON().Object().
HasValue("error", "conflict: requested aggregation is not available for dashboard owner's premium subscription")
})

// public
t.Run("public: no shared_groups filtered", func(t *testing.T) {
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
e.GET(baseUrl, cases[1].Dashboard).
WithQuery("group_ids", "1,2"). // vdb has 4 non-empty groups
WithQuery("aggregation", "hourly").
Expect().Status(http.StatusOK).JSON().Decode(&resp)

assert.Equal(t, 0, len(resp.Data.Categories), "summary chart categories should be empty")
assert.Equal(t, 0, len(resp.Data.Series), "summary chart series should be empty")
})

t.Run("public: no shared_groups success", func(t *testing.T) {
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
e.GET(baseUrl, cases[1].Dashboard).
WithQuery("group_ids", "-1,1,2"). // vdb has 4 non-empty groups
WithQuery("aggregation", "hourly").
Expect().Status(http.StatusOK).JSON().Decode(&resp)

assert.Greater(t, len(resp.Data.Categories), 0, "summary chart categories should not be empty")
require.Equal(t, 1, len(resp.Data.Series), "summary chart series should be aggregated")
assert.Equal(t, api_types.DefaultGroupId, resp.Data.Series[0].Id, "summary chart series should contain default group id")
})

t.Run("public: conflict aggregation", func(t *testing.T) {
e.GET(baseUrl, cases[1].Dashboard).
WithQuery("group_ids", "-1").
WithQuery("aggregation", "daily").
Expect().Status(http.StatusConflict).JSON().Object().
HasValue("error", "conflict: requested aggregation is not available for dashboard owner's premium subscription")
})

t.Run("public: premium shared_groups", func(t *testing.T) {
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
e.GET(baseUrl, cases[3].Dashboard).
WithQuery("group_ids", "1,2,3,100").
WithQuery("aggregation", "weekly").
Expect().Status(http.StatusOK).JSON().Decode(&resp)

assert.Greater(t, len(resp.Data.Categories), 0, "summary chart categories should not be empty")
assert.Greater(t, len(resp.Data.Series), 0, "summary chart series should not be empty")
assert.Equal(t, 3, len(resp.Data.Series), "summary chart series should contain data of exactly 3 groups")
})

t.Run("public: invalid group", func(t *testing.T) {
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
e.GET(baseUrl, cases[3].Dashboard).
WithQuery("group_ids", "100"). // doesn't exist
WithQuery("aggregation", "hourly").
Expect().Status(http.StatusOK).JSON().Decode(&resp)

assert.Equal(t, 0, len(resp.Data.Categories), "summary chart categories should be empty")
assert.Equal(t, 0, len(resp.Data.Series), "summary chart series should be empty")
})

t.Run("public: timeframe", func(t *testing.T) {
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
now := time.Now()
e.GET(baseUrl, cases[3].Dashboard).
WithQuery("group_ids", "-1").
WithQuery("aggregation", "hourly").
WithQuery("before_ts", now.Unix()).
WithQuery("after_ts", now.Add(-time.Hour*2).Unix()).
Expect().Status(http.StatusOK).JSON().Decode(&resp)

for _, category := range resp.Data.Categories {
assert.GreaterOrEqual(t, category, uint64(now.Add(-time.Hour*2).Unix()), "summary chart category should be after requested start")
assert.LessOrEqual(t, category, uint64(now.Unix()), "summary chart category should be before requested end")
}

assert.Greater(t, len(resp.Data.Categories), 0, "summary chart categories should contain at least one entry for timeframe")
assert.LessOrEqual(t, len(resp.Data.Categories), 2, "summary chart categories should contain at most two entries for timeframe")
assert.Greater(t, len(resp.Data.Series), 0, "summary chart series should not be empty")
})

// private
t.Run("private: no premium", func(t *testing.T) {
login(e, cases[2].User)
e.GET(baseUrl, cases[2].Dashboard).
WithQuery("group_ids", "-1").
WithQuery("aggregation", "daily").
Expect().Status(http.StatusConflict).JSON().Object().
HasValue("error", "conflict: requested aggregation is not available for dashboard owner's premium subscription")
})

t.Run("private: premium", func(t *testing.T) {
login(e, cases[4].User)
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
e.GET(baseUrl, cases[4].Dashboard).
WithQuery("group_ids", "-1").
WithQuery("aggregation", "weekly").
Expect().Status(http.StatusOK).JSON().Decode(&resp)

assert.Greater(t, len(resp.Data.Categories), 0, "summary chart categories should not be empty")
assert.Greater(t, len(resp.Data.Series), 0, "summary chart series should not be empty")
})

t.Run("private: proposal efficiency", func(t *testing.T) {
login(e, cases[4].User)
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
e.GET(baseUrl, cases[4].Dashboard).
WithQuery("group_ids", "-1").
WithQuery("aggregation", "weekly").
WithQuery("efficiency_type", "proposal").
Expect().Status(http.StatusOK).JSON().Decode(&resp)

assert.Greater(t, len(resp.Data.Categories), 0, "summary chart categories should not be empty")
assert.Greater(t, len(resp.Data.Series), 0, "summary chart series should not be empty")
})

t.Run("private: efficiency values", func(t *testing.T) {
login(e, cases[6].User)
slot := uint64(3324905) // arbitrary
proposalEpochTime := utils.EpochToTime(utils.EpochOfSlot(slot))

resp := api_types.GetValidatorDashboardSummaryChartResponse{}
e.GET(baseUrl, cases[6].Dashboard).
WithQuery("group_ids", "0").
WithQuery("aggregation", "hourly").
WithQuery("after_ts", proposalEpochTime.Add(-2*time.Hour).Unix()).
WithQuery("before_ts", proposalEpochTime.Add(1*time.Hour).Unix()).
Expect().Status(http.StatusOK).JSON().Decode(&resp)

// make sure summary match & are only counted once
require.Equal(t, 1, len(resp.Data.Series), "summary chart series should contain exactly one entries")

require.Equal(t, 3, len(resp.Data.Series[0].Data), "summary chart series should contain exactly three entries")
assert.Equal(t, 87.9514982445987, resp.Data.Series[0].Data[0], "summary chart el series index 0 should match")
assert.Equal(t, 85.00925779021807, resp.Data.Series[0].Data[1], "summary chart el series index 1 should match")
assert.Equal(t, 86.44134820702745, resp.Data.Series[0].Data[2], "summary chart el series index 2 should match")
})
}

func TestApiDoc(t *testing.T) {
e := httpexpect.WithConfig(getExpectConfig(t, ts))

Expand Down
33 changes: 22 additions & 11 deletions backend/pkg/api/data_access/vdb_summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"maps"
"math"
"math/big"
"slices"
Expand Down Expand Up @@ -997,7 +998,14 @@ func (d *DataAccessService) GetValidatorDashboardSummaryChart(ctx context.Contex
}
}

totalLineRequested := requestedGroupsMap[t.AllGroups]
// need default or all groups for anon dashboards and shared dashboards without group sharing
// TODO could move this to API layer & generalize for all methods
if (dashboardId.Validators != nil && !requestedGroupsMap[t.AllGroups] && !requestedGroupsMap[t.DefaultGroupId]) ||
(dashboardId.AggregateGroups && !requestedGroupsMap[t.AllGroups] && !requestedGroupsMap[t.DefaultGroupId]) {
return ret, nil
}

totalLineRequested := requestedGroupsMap[t.AllGroups] || dashboardId.AggregateGroups
averageNetworkLineRequested := requestedGroupsMap[t.NetworkAverage]

if dashboardId.Validators != nil {
Expand Down Expand Up @@ -1047,6 +1055,7 @@ func (d *DataAccessService) GetValidatorDashboardSummaryChart(ctx context.Contex
// convert the returned data to the expected return type (not pretty)
tsMap := make(map[time.Time]bool)
data := make(map[time.Time]map[int64]float64)
groupMap := make(map[int64]bool)

totalEfficiencyMap := make(map[time.Time]*t.VDBValidatorSummaryChartRow)
for _, row := range queryResults {
Expand All @@ -1056,13 +1065,14 @@ func (d *DataAccessService) GetValidatorDashboardSummaryChart(ctx context.Contex
data[row.Timestamp] = make(map[int64]float64)
}

if requestedGroupsMap[row.GroupId] {
if !dashboardId.AggregateGroups && requestedGroupsMap[row.GroupId] {
groupEfficiency, err := d.calculateChartEfficiency(efficiency, row)
if err != nil {
return nil, err
}

data[row.Timestamp][row.GroupId] = groupEfficiency
groupMap[row.GroupId] = true
}

if totalLineRequested {
Expand Down Expand Up @@ -1092,17 +1102,23 @@ func (d *DataAccessService) GetValidatorDashboardSummaryChart(ctx context.Contex
for ts := range tsMap {
data[ts][int64(t.NetworkAverage)] = averageNetworkEfficiency
}
groupMap[t.NetworkAverage] = true
}

if totalLineRequested {
totalLineGroupId := int64(t.AllGroups)
if dashboardId.AggregateGroups {
totalLineGroupId = t.DefaultGroupId
}
for _, row := range totalEfficiencyMap {
totalEfficiency, err := d.calculateChartEfficiency(efficiency, row)
if err != nil {
return nil, err
}

data[row.Timestamp][t.AllGroups] = totalEfficiency
data[row.Timestamp][totalLineGroupId] = totalEfficiency
}
groupMap[totalLineGroupId] = true
}

tsArray := make([]time.Time, 0, len(tsMap))
Expand All @@ -1113,13 +1129,8 @@ func (d *DataAccessService) GetValidatorDashboardSummaryChart(ctx context.Contex
return tsArray[i].Before(tsArray[j])
})

groupsArray := make([]int64, 0, len(requestedGroupsMap))
for group := range requestedGroupsMap {
groupsArray = append(groupsArray, group)
}
sort.Slice(groupsArray, func(i, j int) bool {
return groupsArray[i] < groupsArray[j]
})
groupsArray := slices.Collect(maps.Keys(groupMap))
slices.Sort(groupsArray)

ret.Categories = make([]uint64, 0, len(tsArray))
for _, ts := range tsArray {
Expand All @@ -1128,7 +1139,7 @@ func (d *DataAccessService) GetValidatorDashboardSummaryChart(ctx context.Contex
ret.Series = make([]t.ChartSeries[int, float64], 0, len(groupsArray))

seriesMap := make(map[int64]*t.ChartSeries[int, float64])
for group := range requestedGroupsMap {
for _, group := range groupsArray {
series := t.ChartSeries[int, float64]{
Id: int(group),
Data: make([]float64, 0, len(tsMap)),
Expand Down

0 comments on commit 90977d8

Please sign in to comment.