@@ -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+
496702func TestApiDoc (t * testing.T ) {
497703 e := httpexpect .WithConfig (getExpectConfig (t , ts ))
498704
0 commit comments