Skip to content

Commit c4dbac8

Browse files
committed
test: summary chart integration tests
1 parent 2bcc21c commit c4dbac8

File tree

2 files changed

+235
-22
lines changed

2 files changed

+235
-22
lines changed

backend/pkg/api/api_test.go

Lines changed: 228 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"testing"
1515
"time"
1616

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

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

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

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

190-
func login(e *httpexpect.Expect) {
194+
func login(e *httpexpect.Expect, user *User) {
195+
if user == nil {
196+
return
197+
}
191198
e.POST("/api/i/login").
192199
WithHeader("Content-Type", "application/json").
193-
WithJSON(map[string]interface{}{"email": "admin@admin.com", "password": "admin"}).
200+
WithJSON(map[string]interface{}{"email": user.Email, "password": user.Password}).
194201
Expect().
195202
Status(http.StatusOK)
196203
}
@@ -247,7 +254,7 @@ func TestInternalLoginHandler(t *testing.T) {
247254
})
248255

249256
t.Run("login with correct user and password", func(t *testing.T) {
250-
login(e)
257+
login(e, &testUsers[0])
251258
})
252259

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

503+
type User struct {
504+
Email string `db:"email"`
505+
Password string
506+
ApiKey string `db:"api_key"`
507+
// optional
508+
Id uint `db:"id" goqu:"omitempty"`
509+
UserGroup string `db:"user_group" goqu:"omitempty"`
510+
EmailConfirmed bool `db:"email_confirmed" goqu:"omitempty"`
511+
}
512+
513+
var testUsers = []User{
514+
{Email: "admin@admin.com", Password: "admin", ApiKey: "admin", UserGroup: api_types.UserGroupAdmin, EmailConfirmed: true},
515+
// holesky
516+
{Id: 122558, Email: "admin2@admin.com", Password: "admin", ApiKey: "admin2", UserGroup: api_types.UserGroupAdmin, EmailConfirmed: true},
517+
{Id: 14, Email: "default@admin.com", Password: "default", ApiKey: "default", EmailConfirmed: true},
518+
{Id: 113321, Email: "admin3@admin.com", Password: "admin", ApiKey: "admin3", UserGroup: api_types.UserGroupAdmin, EmailConfirmed: true},
519+
{Id: 3, Email: "admin4@admin.com", Password: "admin", ApiKey: "admin4", UserGroup: api_types.UserGroupAdmin, EmailConfirmed: true},
520+
}
521+
522+
func TestSummaryChartDetailed(t *testing.T) {
523+
e := httpexpect.WithConfig(getExpectConfig(t, ts))
524+
525+
type TestConfig struct {
526+
Dashboard string
527+
User *User
528+
}
529+
// holesky
530+
cases := []TestConfig{
531+
// anonymous
532+
{Dashboard: "MSwxNTU2MSwxNTY"},
533+
// primary
534+
{Dashboard: "v-009b2943-3268-44f7-a137-2878fc73268b"}, // not shared groups
535+
{Dashboard: "15", User: &testUsers[2]},
536+
// RP
537+
{Dashboard: "v-80d7edaa-74fb-4129-a41e-7700756961cf"}, // shared groups
538+
{Dashboard: "5090", User: &testUsers[1]},
539+
// other
540+
{Dashboard: "5113", User: &testUsers[3]},
541+
// megatron
542+
{Dashboard: "5001", User: &testUsers[4]},
543+
}
544+
545+
baseUrl := "/api/i/validator-dashboards/{id}/summary-chart"
546+
547+
// anonymous
548+
t.Run("anon", func(t *testing.T) {
549+
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
550+
e.GET(baseUrl, cases[0].Dashboard).
551+
WithQuery("group_ids", "-1").
552+
WithQuery("aggregation", "hourly").
553+
Expect().Status(http.StatusOK).JSON().Decode(&resp)
554+
555+
assert.Greater(t, len(resp.Data.Categories), 0, "summary chart categories should not be empty")
556+
require.Greater(t, len(resp.Data.Series), 0, "summary chart series should not be empty")
557+
assert.Equal(t, 1, len(resp.Data.Series), "summary chart series should only contain one group")
558+
assert.Equal(t, api_types.AllGroups, resp.Data.Series[0].Id, "summary chart series should contain default group id")
559+
})
560+
561+
t.Run("anon: conflict aggregation", func(t *testing.T) {
562+
e.GET(baseUrl, cases[0].Dashboard).
563+
WithQuery("group_ids", "-1").
564+
WithQuery("aggregation", "daily").
565+
Expect().Status(http.StatusConflict).JSON().Object().
566+
HasValue("error", "conflict: requested aggregation is not available for dashboard owner's premium subscription")
567+
})
568+
569+
// public
570+
t.Run("public: no shared_groups filtered", func(t *testing.T) {
571+
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
572+
e.GET(baseUrl, cases[1].Dashboard).
573+
WithQuery("group_ids", "1,2"). // vdb has 4 non-empty groups
574+
WithQuery("aggregation", "hourly").
575+
Expect().Status(http.StatusOK).JSON().Decode(&resp)
576+
577+
assert.Equal(t, 0, len(resp.Data.Categories), "summary chart categories should be empty")
578+
assert.Equal(t, 0, len(resp.Data.Series), "summary chart series should be empty")
579+
})
580+
581+
t.Run("public: no shared_groups success", func(t *testing.T) {
582+
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
583+
e.GET(baseUrl, cases[1].Dashboard).
584+
WithQuery("group_ids", "-1,1,2"). // vdb has 4 non-empty groups
585+
WithQuery("aggregation", "hourly").
586+
Expect().Status(http.StatusOK).JSON().Decode(&resp)
587+
588+
assert.Greater(t, len(resp.Data.Categories), 0, "summary chart categories should not be empty")
589+
require.Equal(t, 1, len(resp.Data.Series), "summary chart series should be aggregated")
590+
assert.Equal(t, api_types.DefaultGroupId, resp.Data.Series[0].Id, "summary chart series should contain default group id")
591+
})
592+
593+
t.Run("public: conflict aggregation", func(t *testing.T) {
594+
e.GET(baseUrl, cases[1].Dashboard).
595+
WithQuery("group_ids", "-1").
596+
WithQuery("aggregation", "daily").
597+
Expect().Status(http.StatusConflict).JSON().Object().
598+
HasValue("error", "conflict: requested aggregation is not available for dashboard owner's premium subscription")
599+
})
600+
601+
t.Run("public: premium shared_groups", func(t *testing.T) {
602+
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
603+
e.GET(baseUrl, cases[3].Dashboard).
604+
WithQuery("group_ids", "1,2,3,100").
605+
WithQuery("aggregation", "weekly").
606+
Expect().Status(http.StatusOK).JSON().Decode(&resp)
607+
608+
assert.Greater(t, len(resp.Data.Categories), 0, "summary chart categories should not be empty")
609+
assert.Greater(t, len(resp.Data.Series), 0, "summary chart series should not be empty")
610+
assert.Equal(t, 3, len(resp.Data.Series), "summary chart series should contain data of exactly 3 groups")
611+
})
612+
613+
t.Run("public: invalid group", func(t *testing.T) {
614+
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
615+
e.GET(baseUrl, cases[3].Dashboard).
616+
WithQuery("group_ids", "100"). // doesn't exist
617+
WithQuery("aggregation", "hourly").
618+
Expect().Status(http.StatusOK).JSON().Decode(&resp)
619+
620+
assert.Equal(t, 0, len(resp.Data.Categories), "summary chart categories should be empty")
621+
assert.Equal(t, 0, len(resp.Data.Series), "summary chart series should be empty")
622+
})
623+
624+
t.Run("public: timeframe", func(t *testing.T) {
625+
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
626+
now := time.Now()
627+
e.GET(baseUrl, cases[3].Dashboard).
628+
WithQuery("group_ids", "-1").
629+
WithQuery("aggregation", "hourly").
630+
WithQuery("before_ts", now.Unix()).
631+
WithQuery("after_ts", now.Add(-time.Hour*2).Unix()).
632+
Expect().Status(http.StatusOK).JSON().Decode(&resp)
633+
634+
for _, category := range resp.Data.Categories {
635+
assert.GreaterOrEqual(t, category, uint64(now.Add(-time.Hour*2).Unix()), "summary chart category should be after requested start")
636+
assert.LessOrEqual(t, category, uint64(now.Unix()), "summary chart category should be before requested end")
637+
}
638+
639+
assert.Greater(t, len(resp.Data.Categories), 0, "summary chart categories should contain at least one entry for timeframe")
640+
assert.LessOrEqual(t, len(resp.Data.Categories), 2, "summary chart categories should contain at most two entries for timeframe")
641+
assert.Greater(t, len(resp.Data.Series), 0, "summary chart series should not be empty")
642+
})
643+
644+
// private
645+
t.Run("private: no premium", func(t *testing.T) {
646+
login(e, cases[2].User)
647+
e.GET(baseUrl, cases[2].Dashboard).
648+
WithQuery("group_ids", "-1").
649+
WithQuery("aggregation", "daily").
650+
Expect().Status(http.StatusConflict).JSON().Object().
651+
HasValue("error", "conflict: requested aggregation is not available for dashboard owner's premium subscription")
652+
})
653+
654+
t.Run("private: premium", func(t *testing.T) {
655+
login(e, cases[4].User)
656+
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
657+
e.GET(baseUrl, cases[4].Dashboard).
658+
WithQuery("group_ids", "-1").
659+
WithQuery("aggregation", "weekly").
660+
Expect().Status(http.StatusOK).JSON().Decode(&resp)
661+
662+
assert.Greater(t, len(resp.Data.Categories), 0, "summary chart categories should not be empty")
663+
assert.Greater(t, len(resp.Data.Series), 0, "summary chart series should not be empty")
664+
})
665+
666+
t.Run("private: proposal efficiency", func(t *testing.T) {
667+
login(e, cases[4].User)
668+
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
669+
e.GET(baseUrl, cases[4].Dashboard).
670+
WithQuery("group_ids", "-1").
671+
WithQuery("aggregation", "weekly").
672+
WithQuery("efficiency_type", "proposal").
673+
Expect().Status(http.StatusOK).JSON().Decode(&resp)
674+
675+
assert.Greater(t, len(resp.Data.Categories), 0, "summary chart categories should not be empty")
676+
assert.Greater(t, len(resp.Data.Series), 0, "summary chart series should not be empty")
677+
})
678+
679+
t.Run("private: efficiency values", func(t *testing.T) {
680+
login(e, cases[6].User)
681+
slot := uint64(3324905) // arbitrary
682+
proposalEpochTime := utils.EpochToTime(utils.EpochOfSlot(slot))
683+
684+
resp := api_types.GetValidatorDashboardSummaryChartResponse{}
685+
e.GET(baseUrl, cases[6].Dashboard).
686+
WithQuery("group_ids", "0").
687+
WithQuery("aggregation", "hourly").
688+
WithQuery("after_ts", proposalEpochTime.Add(-2*time.Hour).Unix()).
689+
WithQuery("before_ts", proposalEpochTime.Add(1*time.Hour).Unix()).
690+
Expect().Status(http.StatusOK).JSON().Decode(&resp)
691+
692+
// make sure summary match & are only counted once
693+
require.Equal(t, 1, len(resp.Data.Series), "summary chart series should contain exactly one entries")
694+
695+
require.Equal(t, 3, len(resp.Data.Series[0].Data), "summary chart series should contain exactly three entries")
696+
assert.Equal(t, 87.9514982445987, resp.Data.Series[0].Data[0], "summary chart el series index 0 should match")
697+
assert.Equal(t, 85.00925779021807, resp.Data.Series[0].Data[1], "summary chart el series index 1 should match")
698+
assert.Equal(t, 86.44134820702745, resp.Data.Series[0].Data[2], "summary chart el series index 2 should match")
699+
})
700+
}
701+
496702
func TestApiDoc(t *testing.T) {
497703
e := httpexpect.WithConfig(getExpectConfig(t, ts))
498704

backend/pkg/api/data_access/vdb_summary.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,13 @@ func (d *DataAccessService) GetValidatorDashboardSummaryChart(ctx context.Contex
10311031
}
10321032
}
10331033

1034+
// need default or all groups for anon dashboards and shared dashboards without group sharing
1035+
// TODO could move this to API layer & generalize for all methods
1036+
if (dashboardId.Validators != nil && !requestedGroupsMap[t.AllGroups] && !requestedGroupsMap[t.DefaultGroupId]) ||
1037+
(dashboardId.AggregateGroups && !requestedGroupsMap[t.AllGroups] && !requestedGroupsMap[t.DefaultGroupId]) {
1038+
return ret, nil
1039+
}
1040+
10341041
totalLineRequested := requestedGroupsMap[t.AllGroups] || dashboardId.AggregateGroups
10351042
averageNetworkLineRequested := requestedGroupsMap[t.NetworkAverage]
10361043

0 commit comments

Comments
 (0)