diff --git a/CHANGELOG.md b/CHANGELOG.md index d4ac3f3a..23b32fce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Check if port range valid [\#104](https://github.com/NodeFactoryIo/vedran/pull/104) ([mpetrun5](https://github.com/mpetrun5)) - Valid flag on node [\#105](https://github.com/NodeFactoryIo/vedran/pull/105) ([mpetrun5](https://github.com/mpetrun5)) - Passing SSL certificates [\#112](https://github.com/NodeFactoryIo/vedran/pull/112) ([mpetrun5](https://github.com/mpetrun5)) +- Expose stats endpoints [\#114](https://github.com/NodeFactoryIo/vedran/pull/114) ([MakMuftic](https://github.com/MakMuftic)) ### Fix diff --git a/internal/controllers/ping.go b/internal/controllers/ping.go index c03031c2..021c5db4 100644 --- a/internal/controllers/ping.go +++ b/internal/controllers/ping.go @@ -1,6 +1,7 @@ package controllers import ( + "github.com/NodeFactoryIo/vedran/internal/stats" "math" "net/http" @@ -9,9 +10,7 @@ import ( log "github.com/sirupsen/logrus" ) -const ( - pingIntervalSeconds = 10 -) +const pingOffset = 5 func (c ApiController) PingHandler(w http.ResponseWriter, r *http.Request) { request := r.Context().Value(auth.RequestContextKey).(*auth.RequestContext) @@ -21,7 +20,7 @@ func (c ApiController) PingHandler(w http.ResponseWriter, r *http.Request) { log.Errorf("Unable to calculate node downtime, error: %v", err) } - if math.Abs(downtimeDuration.Seconds()) > pingIntervalSeconds { + if math.Abs(downtimeDuration.Seconds()) > (stats.PingIntervalInSeconds + pingOffset) { downtime := models.Downtime{ Start: lastPingTime, End: request.Timestamp, diff --git a/internal/controllers/ping_test.go b/internal/controllers/ping_test.go index 829c9247..3ff40f78 100644 --- a/internal/controllers/ping_test.go +++ b/internal/controllers/ping_test.go @@ -49,7 +49,7 @@ func TestApiController_PingHandler(t *testing.T) { calculateDowntimeErr: nil, }, { - name: "Saves downtime if downtime duration more than 10 seconds", + name: "Saves downtime if downtime duration more than 5 seconds", statusCode: 200, pingSaveCallCount: 1, pingSaveErr: nil, @@ -65,17 +65,17 @@ func TestApiController_PingHandler(t *testing.T) { pingSaveErr: fmt.Errorf("ERROR"), downtimeSaveErr: nil, downtimeSaveCallCount: 0, - downtimeDuration: time.Duration(time.Second * 9), + downtimeDuration: time.Duration(time.Second * 8), calculateDowntimeErr: nil, }, { - name: "Returns 200 and does not save downtime if downtime duration less than 10 seconds", + name: "Returns 200 and does not save downtime if downtime duration less than 5 + 5 seconds", statusCode: 200, pingSaveCallCount: 1, pingSaveErr: nil, downtimeSaveErr: nil, downtimeSaveCallCount: 0, - downtimeDuration: time.Duration(time.Second * 9), + downtimeDuration: time.Duration(time.Second * 8), calculateDowntimeErr: nil, }, } diff --git a/internal/controllers/stats.go b/internal/controllers/stats.go new file mode 100644 index 00000000..4f029dd3 --- /dev/null +++ b/internal/controllers/stats.go @@ -0,0 +1,52 @@ +package controllers + +import ( + "encoding/json" + "github.com/NodeFactoryIo/vedran/internal/models" + "github.com/NodeFactoryIo/vedran/internal/stats" + muxhelpper "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" + "net/http" +) + +type StatsResponse struct { + Stats map[string]models.NodeStatsDetails `json:"stats"` +} + +func (c *ApiController) StatisticsHandlerAllStats(w http.ResponseWriter, r *http.Request) { + statisticsForPayout, err := stats.CalculateStatisticsFromLastPayout(c.repositories) + if err != nil { + log.Errorf("Failed to calculate statistics, because %v", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(StatsResponse{ + Stats: statisticsForPayout, + }) +} + +func (c *ApiController) StatisticsHandlerStatsForNode(w http.ResponseWriter, r *http.Request) { + vars := muxhelpper.Vars(r) + nodeId, ok := vars["id"] + if !ok || len(nodeId) < 1 { + log.Error("Missing URL parameter node id") + http.NotFound(w, r) + return + } + + nodeStatisticsFromLastPayout, err := stats.CalculateNodeStatisticsFromLastPayout(c.repositories, nodeId) + if err != nil { + log.Errorf("Failed to calculate statistics for node %s, because %v", nodeId, err) + if err.Error() == "not found" { + http.NotFound(w, r) + } else { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(nodeStatisticsFromLastPayout) +} diff --git a/internal/controllers/stats_test.go b/internal/controllers/stats_test.go new file mode 100644 index 00000000..3d42d1ce --- /dev/null +++ b/internal/controllers/stats_test.go @@ -0,0 +1,264 @@ +package controllers + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "github.com/NodeFactoryIo/vedran/internal/models" + "github.com/NodeFactoryIo/vedran/internal/repositories" + mocks "github.com/NodeFactoryIo/vedran/mocks/repositories" + muxhelpper "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestApiController_StatisticsHandlerAllStats(t *testing.T) { + now := time.Now() + tests := []struct { + name string + httpStatus int + nodeId string + // NodeRepo.GetAll + nodeRepoGetAllReturns *[]models.Node + nodeRepoGetAllError error + // RecordRepo.FindSuccessfulRecordsInsideInterval + recordRepoFindSuccessfulRecordsInsideIntervalReturns []models.Record + recordRepoFindSuccessfulRecordsInsideIntervalError error + // DowntimeRepo.FindDowntimesInsideInterval + downtimeRepoFindDowntimesInsideIntervalReturns []models.Downtime + downtimeRepoFindDowntimesInsideIntervalError error + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration time.Duration + pingRepoCalculateDowntimeError error + // PayoutRepo.FindLatestPayout + payoutRepoFindLatestPayoutReturns *models.Payout + payoutRepoFindLatestPayoutError error + // Stats + nodeNumberOfPings float64 + nodeNumberOfRequests float64 + }{ + { + name: "get valid stats", + nodeId: "1", + httpStatus: http.StatusOK, + // NodeRepo.GetAll + nodeRepoGetAllReturns: &[]models.Node{ + { + ID: "1", + }, + }, + nodeRepoGetAllError: nil, + // RecordRepo.FindSuccessfulRecordsInsideInterval + recordRepoFindSuccessfulRecordsInsideIntervalReturns: nil, + recordRepoFindSuccessfulRecordsInsideIntervalError: errors.New("not found"), + // DowntimeRepo.FindDowntimesInsideInterval + downtimeRepoFindDowntimesInsideIntervalReturns: nil, + downtimeRepoFindDowntimesInsideIntervalError: errors.New("not found"), + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 5 * time.Second, + pingRepoCalculateDowntimeError: nil, + // PayoutRepo.FindLatestPayout + payoutRepoFindLatestPayoutReturns: &models.Payout{ + Timestamp: now.Add(-24 * time.Hour), + PaymentDetails: nil, + }, + payoutRepoFindLatestPayoutError: nil, + // Stats + nodeNumberOfRequests: float64(0), + nodeNumberOfPings: float64(8640), + }, + { + name: "unable to get latest interval, server error", + httpStatus: http.StatusInternalServerError, + payoutRepoFindLatestPayoutError: errors.New("db-error"), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // create mock controller + nodeRepoMock := mocks.NodeRepository{} + nodeRepoMock.On("GetAll").Return( + test.nodeRepoGetAllReturns, test.nodeRepoGetAllError, + ) + recordRepoMock := mocks.RecordRepository{} + recordRepoMock.On("FindSuccessfulRecordsInsideInterval", + test.nodeId, mock.Anything, mock.Anything, + ).Return( + test.recordRepoFindSuccessfulRecordsInsideIntervalReturns, + test.recordRepoFindSuccessfulRecordsInsideIntervalError, + ) + metricsRepoMock := mocks.MetricsRepository{} + pingRepoMock := mocks.PingRepository{} + pingRepoMock.On("CalculateDowntime", + test.nodeId, mock.Anything, + ).Return( + time.Now(), + test.pingRepoCalculateDowntimeReturnDuration, + test.pingRepoCalculateDowntimeError, + ) + downtimeRepoMock := mocks.DowntimeRepository{} + downtimeRepoMock.On("FindDowntimesInsideInterval", + test.nodeId, mock.Anything, mock.Anything, + ).Return( + test.downtimeRepoFindDowntimesInsideIntervalReturns, + test.downtimeRepoFindDowntimesInsideIntervalError, + ) + payoutRepoMock := mocks.PayoutRepository{} + payoutRepoMock.On("FindLatestPayout").Return( + test.payoutRepoFindLatestPayoutReturns, + test.payoutRepoFindLatestPayoutError, + ) + apiController := NewApiController(false, repositories.Repos{ + NodeRepo: &nodeRepoMock, + PingRepo: &pingRepoMock, + MetricsRepo: &metricsRepoMock, + RecordRepo: &recordRepoMock, + DowntimeRepo: &downtimeRepoMock, + PayoutRepo: &payoutRepoMock, + }, nil) + handler := http.HandlerFunc(apiController.StatisticsHandlerAllStats) + req, _ := http.NewRequest("GET", "/api/v1/stats", bytes.NewReader(nil)) + rr := httptest.NewRecorder() + + // invoke test request + handler.ServeHTTP(rr, req) + + // asserts + assert.Equal(t, test.httpStatus, rr.Code, fmt.Sprintf("Response status code should be %d", test.httpStatus)) + + var statsResponse StatsResponse + if rr.Code == http.StatusOK { + _ = json.Unmarshal(rr.Body.Bytes(), &statsResponse) + assert.LessOrEqual(t, test.nodeNumberOfPings, statsResponse.Stats[test.nodeId].TotalPings) + assert.Equal(t, test.nodeNumberOfRequests, statsResponse.Stats[test.nodeId].TotalRequests) + } + }) + } +} + +func TestApiController_StatisticsHandlerStatsForNode(t *testing.T) { + now := time.Now() + tests := []struct { + name string + httpStatus int + nodeId string + contextKey string + // RecordRepo.FindSuccessfulRecordsInsideInterval + recordRepoFindSuccessfulRecordsInsideIntervalReturns []models.Record + recordRepoFindSuccessfulRecordsInsideIntervalError error + // DowntimeRepo.FindDowntimesInsideInterval + downtimeRepoFindDowntimesInsideIntervalReturns []models.Downtime + downtimeRepoFindDowntimesInsideIntervalError error + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration time.Duration + pingRepoCalculateDowntimeError error + // PayoutRepo.FindLatestPayout + payoutRepoFindLatestPayoutReturns *models.Payout + payoutRepoFindLatestPayoutError error + // Stats + nodeNumberOfPings float64 + nodeNumberOfRequests float64 + }{ + { + name: "get valid stats", + nodeId: "1", + httpStatus: http.StatusOK, + contextKey: "id", + // RecordRepo.FindSuccessfulRecordsInsideInterval + recordRepoFindSuccessfulRecordsInsideIntervalReturns: nil, + recordRepoFindSuccessfulRecordsInsideIntervalError: errors.New("not found"), + // DowntimeRepo.FindDowntimesInsideInterval + downtimeRepoFindDowntimesInsideIntervalReturns: nil, + downtimeRepoFindDowntimesInsideIntervalError: errors.New("not found"), + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 5 * time.Second, + pingRepoCalculateDowntimeError: nil, + // PayoutRepo.FindLatestPayout + payoutRepoFindLatestPayoutReturns: &models.Payout{ + Timestamp: now.Add(-24 * time.Hour), + PaymentDetails: nil, + }, + payoutRepoFindLatestPayoutError: nil, + // Stats + nodeNumberOfRequests: float64(0), + nodeNumberOfPings: float64(8640), + }, + { + name: "unable to get latest interval, server error", + httpStatus: http.StatusInternalServerError, + contextKey: "id", + payoutRepoFindLatestPayoutError: errors.New("db-error"), + }, + { + name: "unable to find id from request, server error", + httpStatus: http.StatusNotFound, + contextKey: "id", + payoutRepoFindLatestPayoutError: errors.New("not found"), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // create mock controller + recordRepoMock := mocks.RecordRepository{} + recordRepoMock.On("FindSuccessfulRecordsInsideInterval", + test.nodeId, mock.Anything, mock.Anything, + ).Return( + test.recordRepoFindSuccessfulRecordsInsideIntervalReturns, + test.recordRepoFindSuccessfulRecordsInsideIntervalError, + ) + metricsRepoMock := mocks.MetricsRepository{} + pingRepoMock := mocks.PingRepository{} + pingRepoMock.On("CalculateDowntime", + test.nodeId, mock.Anything, + ).Return( + time.Now(), + test.pingRepoCalculateDowntimeReturnDuration, + test.pingRepoCalculateDowntimeError, + ) + downtimeRepoMock := mocks.DowntimeRepository{} + downtimeRepoMock.On("FindDowntimesInsideInterval", + test.nodeId, mock.Anything, mock.Anything, + ).Return( + test.downtimeRepoFindDowntimesInsideIntervalReturns, + test.downtimeRepoFindDowntimesInsideIntervalError, + ) + payoutRepoMock := mocks.PayoutRepository{} + payoutRepoMock.On("FindLatestPayout").Return( + test.payoutRepoFindLatestPayoutReturns, + test.payoutRepoFindLatestPayoutError, + ) + apiController := NewApiController(false, repositories.Repos{ + PingRepo: &pingRepoMock, + MetricsRepo: &metricsRepoMock, + RecordRepo: &recordRepoMock, + DowntimeRepo: &downtimeRepoMock, + PayoutRepo: &payoutRepoMock, + }, nil) + type ContextKey string + req, _ := http.NewRequest("GET", "/api/v1/stats/node/1", bytes.NewReader(nil)) + req = req.WithContext(context.WithValue(req.Context(), ContextKey(test.contextKey), "1")) + rr := httptest.NewRecorder() + + // invoke test request + router := muxhelpper.NewRouter() + router.HandleFunc("/api/v1/stats/node/{id}", apiController.StatisticsHandlerStatsForNode) + router.ServeHTTP(rr, req) + + // asserts + assert.Equal(t, test.httpStatus, rr.Code, fmt.Sprintf("Response status code should be %d", test.httpStatus)) + + var statsResponse models.NodeStatsDetails + if rr.Code == http.StatusOK { + _ = json.Unmarshal(rr.Body.Bytes(), &statsResponse) + assert.LessOrEqual(t, test.nodeNumberOfPings, statsResponse.TotalPings) + assert.Equal(t, test.nodeNumberOfRequests, statsResponse.TotalRequests) + } + }) + } +} diff --git a/internal/loadbalancer/server.go b/internal/loadbalancer/server.go index 0c84baa2..a8404ea5 100644 --- a/internal/loadbalancer/server.go +++ b/internal/loadbalancer/server.go @@ -39,6 +39,7 @@ func StartLoadBalancerServer(props configuration.Configuration) { repos.RecordRepo = repositories.NewRecordRepo(database) repos.NodeRepo = repositories.NewNodeRepo(database) repos.DowntimeRepo = repositories.NewDowntimeRepo(database) + repos.PayoutRepo = repositories.NewPayoutRepo(database) err = repos.PingRepo.ResetAllPings() if err != nil { log.Fatalf("Failed reseting pings because of: %v", err) diff --git a/internal/models/payout.go b/internal/models/payout.go new file mode 100644 index 00000000..85f5e696 --- /dev/null +++ b/internal/models/payout.go @@ -0,0 +1,13 @@ +package models + +import "time" + +type Payout struct { + Timestamp time.Time `json:"timestamp"` + PaymentDetails map[string]NodeStatsDetails +} + +type NodeStatsDetails struct { + TotalPings float64 `json:"total_pings"` + TotalRequests float64 `json:"total_requests"` +} diff --git a/internal/repositories/downtime.go b/internal/repositories/downtime.go index fda370e2..a11cd4e3 100644 --- a/internal/repositories/downtime.go +++ b/internal/repositories/downtime.go @@ -3,10 +3,15 @@ package repositories import ( "github.com/NodeFactoryIo/vedran/internal/models" "github.com/asdine/storm/v3" + "github.com/asdine/storm/v3/q" + "time" ) type DowntimeRepository interface { Save(downtime *models.Downtime) error + // FindDowntimesInsideInterval returns all models.Downtime that started or ended inside interval + // defined with arguments from and to + FindDowntimesInsideInterval(nodeID string, from time.Time, to time.Time) ([]models.Downtime, error) } type DowntimeRepo struct { @@ -22,3 +27,22 @@ func NewDowntimeRepo(db *storm.DB) *DowntimeRepo { func (r *DowntimeRepo) Save(downtime *models.Downtime) error { return r.db.Save(downtime) } + +func (r *DowntimeRepo) FindDowntimesInsideInterval(nodeID string, from time.Time, to time.Time) ([]models.Downtime, error) { + var downtimes []models.Downtime + err := r.db.Select(q.And( + q.Eq("id", nodeID), + q.Or( + q.And( // start inside interval + q.Gte("start", from), + q.Lte("start", to), + ), + q.And( // end inside interval + q.Gte("end", from), + q.Lte("end", to), + ), + ), + )).Find(downtimes) + return downtimes, err +} + diff --git a/internal/repositories/payout.go b/internal/repositories/payout.go new file mode 100644 index 00000000..68ebf1d5 --- /dev/null +++ b/internal/repositories/payout.go @@ -0,0 +1,39 @@ +package repositories + +import ( + "github.com/NodeFactoryIo/vedran/internal/models" + "github.com/asdine/storm/v3" +) + +type PayoutRepository interface { + Save(payment *models.Payout) error + GetAll() (*[]models.Payout, error) + FindLatestPayout() (*models.Payout, error) +} + +type payoutRepo struct { + db *storm.DB +} + +func NewPayoutRepo(db *storm.DB) PayoutRepository { + return &payoutRepo{ + db: db, + } +} + +func (p *payoutRepo) Save(payment *models.Payout) error { + return p.db.Save(payment) +} + +func (p *payoutRepo) GetAll() (*[]models.Payout, error) { + var payouts []models.Payout + err := p.db.All(&payouts) + return &payouts, err +} + +func (p *payoutRepo) FindLatestPayout() (*models.Payout, error) { + var payout models.Payout + err := p.db.Select().OrderBy("Timestamp").First(&payout) + return &payout, err +} + diff --git a/internal/repositories/record.go b/internal/repositories/record.go index 00e88da3..7e594023 100644 --- a/internal/repositories/record.go +++ b/internal/repositories/record.go @@ -3,10 +3,15 @@ package repositories import ( "github.com/NodeFactoryIo/vedran/internal/models" "github.com/asdine/storm/v3" + "github.com/asdine/storm/v3/q" + "time" ) type RecordRepository interface { Save(record *models.Record) error + // FindSuccessfulRecordsInsideInterval returns all models.Record that happened inside interval + // defined with arguments from and to + FindSuccessfulRecordsInsideInterval(nodeID string, from time.Time, to time.Time) ([]models.Record, error) } type recordRepo struct { @@ -22,3 +27,14 @@ func NewRecordRepo(db *storm.DB) RecordRepository { func (r *recordRepo) Save(record *models.Record) error { return r.db.Save(record) } + +func (r *recordRepo) FindSuccessfulRecordsInsideInterval(nodeID string, from time.Time, to time.Time) ([]models.Record, error) { + var records []models.Record + err := r.db.Select(q.And( + q.Eq("id", nodeID), + q.Gte("timestamp", from), + q.Lte("timestamp", to), + q.Eq("status", "successful"), + )).Find(&records) + return records, err +} diff --git a/internal/repositories/repos.go b/internal/repositories/repos.go index c5ddccb9..d610d469 100644 --- a/internal/repositories/repos.go +++ b/internal/repositories/repos.go @@ -7,4 +7,5 @@ type Repos struct { MetricsRepo MetricsRepository RecordRepo RecordRepository DowntimeRepo DowntimeRepository + PayoutRepo PayoutRepository } diff --git a/internal/router/routes.go b/internal/router/routes.go index cab670bd..5ee5de59 100644 --- a/internal/router/routes.go +++ b/internal/router/routes.go @@ -27,4 +27,6 @@ func createRoutes(apiController *controllers.ApiController, router *mux.Router) createRoute("/api/v1/nodes", "POST", apiController.RegisterHandler, router, false) createRoute("/api/v1/nodes/pings", "POST", apiController.PingHandler, router, true) createRoute("/api/v1/nodes/metrics", "PUT", apiController.SaveMetricsHandler, router, true) + createRoute("/api/v1/stats", "GET", apiController.StatisticsHandlerAllStats, router, false) + createRoute("/api/v1/stats/node/{id}", "GET", apiController.StatisticsHandlerStatsForNode, router, false) } diff --git a/internal/stats/const.go b/internal/stats/const.go new file mode 100644 index 00000000..2a8e5b78 --- /dev/null +++ b/internal/stats/const.go @@ -0,0 +1,6 @@ +package stats + +const ( + // PingIntervalInSeconds + PingIntervalInSeconds = 5 +) \ No newline at end of file diff --git a/internal/stats/interval.go b/internal/stats/interval.go new file mode 100644 index 00000000..1c9fdab8 --- /dev/null +++ b/internal/stats/interval.go @@ -0,0 +1,21 @@ +package stats + +import ( + "github.com/NodeFactoryIo/vedran/internal/repositories" + "time" +) + +var nowFunc = time.Now + +// GetIntervalFromLastPayout returns interval from last recorded payout until now as (intervalStart, intervalEnd, err) +func GetIntervalFromLastPayout(repos repositories.Repos) (*time.Time, *time.Time, error) { + latestPayout, err := repos.PayoutRepo.FindLatestPayout() + if err != nil { + return nil, nil, err + } + + intervalStart := latestPayout.Timestamp + intervalEnd := nowFunc() + + return &intervalStart, &intervalEnd, err +} \ No newline at end of file diff --git a/internal/stats/interval_test.go b/internal/stats/interval_test.go new file mode 100644 index 00000000..0c964a18 --- /dev/null +++ b/internal/stats/interval_test.go @@ -0,0 +1,67 @@ +package stats + +import ( + "errors" + "github.com/NodeFactoryIo/vedran/internal/models" + "github.com/NodeFactoryIo/vedran/internal/repositories" + mocks "github.com/NodeFactoryIo/vedran/mocks/repositories" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func Test_GetIntervalFromLastPayout(t *testing.T) { + now := time.Now() + hourAgo := now.Add(-24 * time.Hour) + nowFunc = func() time.Time { + return now + } + + tests := []struct { + name string + payoutRepoFindLatestPayoutReturns *models.Payout + payoutRepoFindLatestPayoutError error + getIntervalFromLastPayoutIntervalStart *time.Time + getIntervalFromLastPayoutIntervalEnd *time.Time + getIntervalFromLastPayoutError error + }{ + { + name: "calculate interval from existing last payment to now", + payoutRepoFindLatestPayoutReturns: &models.Payout{ + Timestamp: now.Add(-24 * time.Hour), + PaymentDetails: nil, + }, + payoutRepoFindLatestPayoutError: nil, + getIntervalFromLastPayoutIntervalStart: &hourAgo, + getIntervalFromLastPayoutIntervalEnd: &now, + getIntervalFromLastPayoutError: nil, + }, + { + name: "calculate interval from existing last payment to now", + payoutRepoFindLatestPayoutReturns: nil, + payoutRepoFindLatestPayoutError: errors.New("db error"), + getIntervalFromLastPayoutIntervalStart: nil, + getIntervalFromLastPayoutIntervalEnd: nil, + getIntervalFromLastPayoutError: errors.New("db error"), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + payoutRepoMock := mocks.PayoutRepository{} + payoutRepoMock.On("FindLatestPayout").Return( + test.payoutRepoFindLatestPayoutReturns, + test.payoutRepoFindLatestPayoutError, + ) + repos := repositories.Repos{ + PayoutRepo: &payoutRepoMock, + } + + intervalStart, intervalEnd, err := GetIntervalFromLastPayout(repos) + assert.Equal(t, test.getIntervalFromLastPayoutIntervalStart, intervalStart) + assert.Equal(t, test.getIntervalFromLastPayoutIntervalEnd, intervalEnd) + assert.Equal(t, test.getIntervalFromLastPayoutError, err) + }) + } + + nowFunc = time.Now +} diff --git a/internal/stats/ping.go b/internal/stats/ping.go new file mode 100644 index 00000000..460026cc --- /dev/null +++ b/internal/stats/ping.go @@ -0,0 +1,54 @@ +package stats + +import ( + "github.com/NodeFactoryIo/vedran/internal/models" + "github.com/NodeFactoryIo/vedran/internal/repositories" + "math" + "time" +) + +func CalculateTotalPingsForNode( + repos repositories.Repos, + nodeId string, + intervalStart time.Time, + intervalEnd time.Time, +) (float64, error) { + downtimesInInterval, err := repos.DowntimeRepo.FindDowntimesInsideInterval(nodeId, intervalStart, intervalEnd) + if err != nil { + if err.Error() == "not found" { + downtimesInInterval = []models.Downtime{} + } else { + return 0, err + } + } + + totalTime := intervalEnd.Sub(intervalStart) + leftTime := totalTime + for _, downtime := range downtimesInInterval { + var downtimeLength time.Duration + // case 1: entire downtime inside interval + if downtime.Start.After(intervalStart) && downtime.End.Before(intervalEnd) { + downtimeLength = downtime.End.Sub(downtime.Start) + } + // case 2: downtime started before interval + if downtime.Start.Before(intervalStart) { + downtimeLength = downtime.End.Sub(intervalStart) + } + leftTime -= downtimeLength + } + // case 3: downtime still active + _, duration, err := repos.PingRepo.CalculateDowntime(nodeId, intervalEnd) + if err != nil { + return 0, err + } + if duration.Seconds() > leftTime.Seconds() { + // if node was down for entire observed interval + return 0, nil + } + if math.Abs(duration.Seconds()) > PingIntervalInSeconds { + leftTime -= duration + } + + totalPings := leftTime.Seconds() / PingIntervalInSeconds + return totalPings, nil +} \ No newline at end of file diff --git a/internal/stats/ping_test.go b/internal/stats/ping_test.go new file mode 100644 index 00000000..acc016c7 --- /dev/null +++ b/internal/stats/ping_test.go @@ -0,0 +1,258 @@ +package stats + +import ( + "errors" + "github.com/NodeFactoryIo/vedran/internal/models" + "github.com/NodeFactoryIo/vedran/internal/repositories" + mocks "github.com/NodeFactoryIo/vedran/mocks/repositories" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func Test_CalculateTotalPingsForNode(t *testing.T) { + now := time.Now() + + tests := []struct { + name string + nodeID string + intervalStart time.Time + intervalEnd time.Time + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns []models.Downtime + downtimeRepoFindDowntimesInsideIntervalError error + downtimeRepoFindDowntimesInsideIntervalNumOfCalls int + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration time.Duration + pingRepoCalculateDowntimeError error + pingRepoCalculateDowntimeNumOfCalls int + // + calculateTotalPingsForNodeError error + calculateTotalPingsForNodeReturns float64 + }{ + { + name: "no downtimes", + nodeID: "1", + // interval of 24 hours + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns: nil, + downtimeRepoFindDowntimesInsideIntervalError: errors.New("not found"), + downtimeRepoFindDowntimesInsideIntervalNumOfCalls: 1, + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 5 * time.Second, + pingRepoCalculateDowntimeError: nil, + pingRepoCalculateDowntimeNumOfCalls: 1, + // [total_interval - total_downtime] / ping_interval = num_of_pings + // [24h (86400s) - 0min (0s)] / 10 = 8640 + calculateTotalPingsForNodeReturns: float64(86400 / PingIntervalInSeconds), + calculateTotalPingsForNodeError: nil, + }, + { + name: "multiple downtimes inside interval", + nodeID: "1", + // interval of 24 hours + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns: []models.Downtime{ + { // downtime 1h + ID: 1, + NodeId: "1", + Start: now.Add(-11 * time.Hour), + End: now.Add(-10 * time.Hour), + }, + { // downtime 20min + ID: 2, + NodeId: "1", + Start: now.Add(-120 * time.Minute), + End: now.Add(-100 * time.Minute), + }, + { // downtime 20s + ID: 3, + NodeId: "1", + Start: now.Add(-120 * time.Second), + End: now.Add(-100 * time.Second), + }, + }, + downtimeRepoFindDowntimesInsideIntervalError: nil, + downtimeRepoFindDowntimesInsideIntervalNumOfCalls: 1, + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 1 * time.Second, + pingRepoCalculateDowntimeError: nil, + pingRepoCalculateDowntimeNumOfCalls: 1, + // [total_interval - total_downtime] / ping_interval = num_of_pings + // [24h (86400s) - 1h20min20s (4820s)] / 10 = 8158 + calculateTotalPingsForNodeReturns: float64(81580 / PingIntervalInSeconds), + calculateTotalPingsForNodeError: nil, + }, + { + name: "downtime started before interval", + nodeID: "1", + // interval of 24 hours + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns: []models.Downtime{ + { // downtime 1h30min (30min effective), started before calculated interval + ID: 1, + NodeId: "1", + Start: now.Add(-1500 * time.Minute), // -24h60min + End: now.Add(-1410 * time.Minute), // -23h30min + }, + }, + downtimeRepoFindDowntimesInsideIntervalError: nil, + downtimeRepoFindDowntimesInsideIntervalNumOfCalls: 1, + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 1 * time.Second, + pingRepoCalculateDowntimeError: nil, + pingRepoCalculateDowntimeNumOfCalls: 1, + // [total_interval - total_downtime] / ping_interval = num_of_pings + // [24h (86400s) - 30min (1800s)] / 10 = 8460 + calculateTotalPingsForNodeReturns: float64(84600 / PingIntervalInSeconds), + calculateTotalPingsForNodeError: nil, + }, + { + name: "downtime still active", + nodeID: "1", + // interval of 24 hours + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns: []models.Downtime{}, + downtimeRepoFindDowntimesInsideIntervalError: nil, + downtimeRepoFindDowntimesInsideIntervalNumOfCalls: 1, + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 30 * time.Minute, + pingRepoCalculateDowntimeError: nil, + pingRepoCalculateDowntimeNumOfCalls: 1, + // [total_interval - total_downtime] / ping_interval = num_of_pings + // [24h (86400s) - 30min (1800s)] / 10 = 8460 + calculateTotalPingsForNodeReturns: float64(84600 / PingIntervalInSeconds), + calculateTotalPingsForNodeError: nil, + }, + { + name: "mixed multiple downtimes", + nodeID: "1", + // interval of 24 hours + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns: []models.Downtime{ + { // downtime 1h20min (20min effective), started before calculated interval + ID: 1, + NodeId: "1", + Start: now.Add(-1500 * time.Minute), // -24h60min + End: now.Add(-1420 * time.Minute), // -23h40min + }, + { // downtime 20min + ID: 2, + NodeId: "1", + Start: now.Add(-220 * time.Minute), + End: now.Add(-200 * time.Minute), + }, + }, + downtimeRepoFindDowntimesInsideIntervalError: nil, + downtimeRepoFindDowntimesInsideIntervalNumOfCalls: 1, + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 20 * time.Minute, + pingRepoCalculateDowntimeError: nil, + pingRepoCalculateDowntimeNumOfCalls: 1, + // [total_interval - total_downtime] / ping_interval = num_of_pings + // [24h (86400s) - 3x20min (3600s)] / 10 = 8280 + calculateTotalPingsForNodeReturns: float64(82800 / PingIntervalInSeconds), + calculateTotalPingsForNodeError: nil, + }, + { + name: "error on fetching downtime", + nodeID: "1", + // interval of 24 hours + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns: nil, + downtimeRepoFindDowntimesInsideIntervalError: errors.New("db error"), + downtimeRepoFindDowntimesInsideIntervalNumOfCalls: 1, + // CalculateTotalPingsForNode + calculateTotalPingsForNodeReturns: float64(0), + calculateTotalPingsForNodeError: errors.New("db error"), + }, + { + name: "error on fetching downtime", + nodeID: "1", + // interval of 24 hours + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns: []models.Downtime{}, + downtimeRepoFindDowntimesInsideIntervalError: nil, + downtimeRepoFindDowntimesInsideIntervalNumOfCalls: 1, + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 0 * time.Second, + pingRepoCalculateDowntimeError: errors.New("db error"), + pingRepoCalculateDowntimeNumOfCalls: 1, + // CalculateTotalPingsForNode + calculateTotalPingsForNodeReturns: float64(0), + calculateTotalPingsForNodeError: errors.New("db error"), + }, + { + name: "downtime bigger than interval", + nodeID: "1", + // interval of 24 hours + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns: []models.Downtime{}, + downtimeRepoFindDowntimesInsideIntervalError: nil, + downtimeRepoFindDowntimesInsideIntervalNumOfCalls: 1, + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 26 * time.Hour, + pingRepoCalculateDowntimeError: nil, + pingRepoCalculateDowntimeNumOfCalls: 1, + // CalculateTotalPingsForNode + calculateTotalPingsForNodeReturns: float64(0), + calculateTotalPingsForNodeError: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + downtimeRepoMock := mocks.DowntimeRepository{} + downtimeRepoMock.On("FindDowntimesInsideInterval", + test.nodeID, test.intervalStart, test.intervalEnd, + ).Return( + test.downtimeRepoFindDowntimesInsideIntervalReturns, + test.downtimeRepoFindDowntimesInsideIntervalError, + ) + + pingRepoMock := mocks.PingRepository{} + pingRepoMock.On("CalculateDowntime", + test.nodeID, test.intervalEnd, + ).Return( + time.Now(), + test.pingRepoCalculateDowntimeReturnDuration, + test.pingRepoCalculateDowntimeError, + ) + + repos := repositories.Repos{ + PingRepo: &pingRepoMock, + DowntimeRepo: &downtimeRepoMock, + } + + totalPings, err := CalculateTotalPingsForNode(repos, test.nodeID, test.intervalStart, test.intervalEnd) + + assert.Equal(t, err, test.calculateTotalPingsForNodeError) + assert.Equal(t, test.calculateTotalPingsForNodeReturns, totalPings) + + downtimeRepoMock.AssertNumberOfCalls(t, + "FindDowntimesInsideInterval", + test.downtimeRepoFindDowntimesInsideIntervalNumOfCalls, + ) + pingRepoMock.AssertNumberOfCalls(t, + "CalculateDowntime", + test.pingRepoCalculateDowntimeNumOfCalls, + ) + }) + } +} diff --git a/internal/stats/stats.go b/internal/stats/stats.go new file mode 100644 index 00000000..1412c4d3 --- /dev/null +++ b/internal/stats/stats.go @@ -0,0 +1,87 @@ +package stats + +import ( + "github.com/NodeFactoryIo/vedran/internal/models" + "github.com/NodeFactoryIo/vedran/internal/repositories" + log "github.com/sirupsen/logrus" + "time" +) + +// CalculateStatisticsFromLastPayout calculates stats for all nodes for interval, that starts from last recorded payout +// until now, as map[string]models.NodeStatsDetails where keys represent node id-s +func CalculateStatisticsFromLastPayout(repos repositories.Repos) (map[string]models.NodeStatsDetails, error) { + intervalStart, intervalEnd, err := GetIntervalFromLastPayout(repos) + if err != nil { + return nil, err + } + return CalculateStatisticsForInterval(repos, *intervalStart, *intervalEnd) +} + +// CalculateNodeStatisticsFromLastPayout calculates stats for specific node for interval, that starts from last recorded payout +// until now, as models.NodeStatsDetails where node is specified with argument nodeId +func CalculateNodeStatisticsFromLastPayout(repos repositories.Repos, nodeId string) (*models.NodeStatsDetails, error) { + intervalStart, intervalEnd, err := GetIntervalFromLastPayout(repos) + if err != nil { + return nil, err + } + return CalculateNodeStatisticsForInterval(repos, nodeId, *intervalStart, *intervalEnd) +} + +// CalculateStatisticsForInterval calculates stats for all nodes for interval, specified with arguments +// intervalStart and intervalEnd, as map[string]models.NodeStatsDetails where keys represent node id-s +func CalculateStatisticsForInterval( + repos repositories.Repos, + intervalStart time.Time, + intervalEnd time.Time, +) (map[string]models.NodeStatsDetails, error) { + + allNodes, err := repos.NodeRepo.GetAll() + if err != nil { + if err.Error() == "not found" { + log.Debugf("Unable to calculate statistics if there isn't any saved nodes") + } + return nil, err + } + + var allNodesStats = make(map[string]models.NodeStatsDetails) + for _, node := range *allNodes { + nodeStats, err := CalculateNodeStatisticsForInterval(repos, node.ID, intervalStart, intervalEnd) + if err != nil { + return nil, err + } + allNodesStats[node.ID] = *nodeStats + } + + return allNodesStats, nil +} + +// CalculateNodeStatisticsForInterval calculates stats for specific node for interval, specified with arguments +// intervalStart and intervalEnd, as models.NodeStatsDetails where node is specified with argument nodeId +func CalculateNodeStatisticsForInterval( + repos repositories.Repos, + nodeId string, + intervalStart time.Time, + intervalEnd time.Time, +) (*models.NodeStatsDetails, error) { + recordsInInterval, err := repos.RecordRepo.FindSuccessfulRecordsInsideInterval(nodeId, intervalStart, intervalEnd) + if err != nil { + if err.Error() == "not found" { + recordsInInterval = []models.Record{} + } else { + return nil, err + } + } + + totalPings, err := CalculateTotalPingsForNode(repos, nodeId, intervalStart, intervalEnd) + if err != nil { + log.Errorf("Unable to calculate total number of pings for node %s, because %v", nodeId, err) + return nil, err + } + + return &models.NodeStatsDetails{ + TotalPings: totalPings, + TotalRequests: float64(len(recordsInInterval)), + }, nil +} + + diff --git a/internal/stats/stats_test.go b/internal/stats/stats_test.go new file mode 100644 index 00000000..a4bbd45a --- /dev/null +++ b/internal/stats/stats_test.go @@ -0,0 +1,641 @@ +package stats + +import ( + "errors" + "github.com/NodeFactoryIo/vedran/internal/models" + "github.com/NodeFactoryIo/vedran/internal/repositories" + mocks "github.com/NodeFactoryIo/vedran/mocks/repositories" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func Test_CalculateNodeStatisticsFromLastPayout(t *testing.T) { + now := time.Now() + nowFunc = func() time.Time { + return now + } + tests := []struct { + name string + nodeID string + intervalStart time.Time + intervalEnd time.Time + // RecordRepo.FindByNodeID + recordRepoFindSuccessfulRecordsInsideIntervalReturns []models.Record + recordRepoFindSuccessfulRecordsInsideIntervalError error + recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls int + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns []models.Downtime + downtimeRepoFindDowntimesInsideIntervalError error + downtimeRepoFindDowntimesInsideIntervalNumOfCalls int + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration time.Duration + pingRepoCalculateDowntimeError error + pingRepoCalculateDowntimeNumOfCalls int + // PayoutRepo.FindLatestPayout + payoutRepoFindLatestPayoutReturns *models.Payout + payoutRepoFindLatestPayoutError error + payoutRepoFindLatestPayoutNumOfCalls int + // CalculateNodeStatisticsForInterval + calculateNodeStatisticsFromLastPayoutReturns *models.NodeStatsDetails + calculateNodeStatisticsFromLastPayoutError error + }{ + { + name: "valid statistics with no records and no downtime", + nodeID: "1", + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // RecordRepo.FindByNodeID + recordRepoFindSuccessfulRecordsInsideIntervalReturns: nil, + recordRepoFindSuccessfulRecordsInsideIntervalError: errors.New("not found"), + recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls: 1, + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns: nil, + downtimeRepoFindDowntimesInsideIntervalError: errors.New("not found"), + downtimeRepoFindDowntimesInsideIntervalNumOfCalls: 1, + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 5 * time.Second, + pingRepoCalculateDowntimeError: nil, + pingRepoCalculateDowntimeNumOfCalls: 1, + // PayoutRepo.FindLatestPayout + payoutRepoFindLatestPayoutReturns: &models.Payout{ + Timestamp: now.Add(-24 * time.Hour), + PaymentDetails: nil, + }, + payoutRepoFindLatestPayoutError: nil, + payoutRepoFindLatestPayoutNumOfCalls: 1, + // CalculateNodeStatisticsForInterval + calculateNodeStatisticsFromLastPayoutReturns: &models.NodeStatsDetails{ + TotalPings: 17280, // no downtime - max number of pings + TotalRequests: 0, + }, + calculateNodeStatisticsFromLastPayoutError: nil, + }, + { + name: "error on fetching latest payout", + nodeID: "1", + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // PayoutRepo.FindLatestPayout + payoutRepoFindLatestPayoutReturns: nil, + payoutRepoFindLatestPayoutError: errors.New("db error"), + payoutRepoFindLatestPayoutNumOfCalls: 1, + // CalculateNodeStatisticsForInterval + calculateNodeStatisticsFromLastPayoutReturns: nil, + calculateNodeStatisticsFromLastPayoutError: errors.New("db error"), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // create mock controller + recordRepoMock := mocks.RecordRepository{} + recordRepoMock.On("FindSuccessfulRecordsInsideInterval", + test.nodeID, test.intervalStart, test.intervalEnd, + ).Return( + test.recordRepoFindSuccessfulRecordsInsideIntervalReturns, + test.recordRepoFindSuccessfulRecordsInsideIntervalError, + ) + downtimeRepoMock := mocks.DowntimeRepository{} + downtimeRepoMock.On("FindDowntimesInsideInterval", + test.nodeID, test.intervalStart, test.intervalEnd, + ).Return( + test.downtimeRepoFindDowntimesInsideIntervalReturns, + test.downtimeRepoFindDowntimesInsideIntervalError, + ) + pingRepoMock := mocks.PingRepository{} + pingRepoMock.On("CalculateDowntime", + test.nodeID, test.intervalEnd, + ).Return( + time.Now(), + test.pingRepoCalculateDowntimeReturnDuration, + test.pingRepoCalculateDowntimeError, + ) + payoutRepoMock := mocks.PayoutRepository{} + payoutRepoMock.On("FindLatestPayout").Return( + test.payoutRepoFindLatestPayoutReturns, + test.payoutRepoFindLatestPayoutError, + ) + repos := repositories.Repos{ + PingRepo: &pingRepoMock, + RecordRepo: &recordRepoMock, + DowntimeRepo: &downtimeRepoMock, + PayoutRepo: &payoutRepoMock, + } + + statisticsForPayout, err := CalculateNodeStatisticsFromLastPayout(repos, test.nodeID) + + assert.Equal(t, test.calculateNodeStatisticsFromLastPayoutError, err) + assert.Equal(t, test.calculateNodeStatisticsFromLastPayoutReturns, statisticsForPayout) + + recordRepoMock.AssertNumberOfCalls(t, + "FindSuccessfulRecordsInsideInterval", + test.recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls, + ) + downtimeRepoMock.AssertNumberOfCalls(t, + "FindDowntimesInsideInterval", + test.downtimeRepoFindDowntimesInsideIntervalNumOfCalls, + ) + pingRepoMock.AssertNumberOfCalls(t, + "CalculateDowntime", + test.pingRepoCalculateDowntimeNumOfCalls, + ) + payoutRepoMock.AssertNumberOfCalls(t, + "FindLatestPayout", + test.payoutRepoFindLatestPayoutNumOfCalls, + ) + }) + } + nowFunc = time.Now +} + +func Test_CalculateStatisticsFromLastPayout(t *testing.T) { + now := time.Now() + nowFunc = func() time.Time { + return now + } + + tests := []struct { + name string + nodeID string + intervalStart time.Time + intervalEnd time.Time + // NodeRepo.GetAll + nodeRepoGetAllReturns *[]models.Node + nodeRepoGetAllError error + nodeRepoGetAllNumOfCalls int + // RecordRepo.FindByNodeID + recordRepoFindSuccessfulRecordsInsideIntervalReturns []models.Record + recordRepoFindSuccessfulRecordsInsideIntervalError error + recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls int + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns []models.Downtime + downtimeRepoFindDowntimesInsideIntervalError error + downtimeRepoFindDowntimesInsideIntervalNumOfCalls int + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration time.Duration + pingRepoCalculateDowntimeError error + pingRepoCalculateDowntimeNumOfCalls int + // PayoutRepo.FindLatestPayout + payoutRepoFindLatestPayoutReturns *models.Payout + payoutRepoFindLatestPayoutError error + payoutRepoFindLatestPayoutNumOfCalls int + // CalculateNodeStatisticsForInterval + calculateStatisticsFromLastPayoutReturns map[string]models.NodeStatsDetails + calculateStatisticsFromLastPayoutError error + }{ + { + name: "valid statistics with multiple records and no downtime", + nodeID: "1", + // interval of 24 hours + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // NodeRepo.GetAll + nodeRepoGetAllReturns: &[]models.Node{ + { + ID: "1", + }, + }, + nodeRepoGetAllError: nil, + nodeRepoGetAllNumOfCalls: 1, + // RecordRepo.FindByNodeID + recordRepoFindSuccessfulRecordsInsideIntervalReturns: nil, + recordRepoFindSuccessfulRecordsInsideIntervalError: errors.New("not found"), + recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls: 1, + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns: nil, + downtimeRepoFindDowntimesInsideIntervalError: errors.New("not found"), + downtimeRepoFindDowntimesInsideIntervalNumOfCalls: 1, + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 5 * time.Second, + pingRepoCalculateDowntimeError: nil, + pingRepoCalculateDowntimeNumOfCalls: 1, + // PayoutRepo.FindLatestPayout + payoutRepoFindLatestPayoutReturns: &models.Payout{ + Timestamp: now.Add(-24 * time.Hour), + PaymentDetails: nil, + }, + payoutRepoFindLatestPayoutError: nil, + payoutRepoFindLatestPayoutNumOfCalls: 1, + // CalculateNodeStatisticsForInterval + calculateStatisticsFromLastPayoutReturns: map[string]models.NodeStatsDetails{ + "1": { + TotalPings: 17280, // no downtime - max number of pings + TotalRequests: 0, + }, + }, + calculateStatisticsFromLastPayoutError: nil, + }, + { + name: "error on fetching latest payout", + nodeID: "1", + // interval of 24 hours + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // PayoutRepo.FindLatestPayout + payoutRepoFindLatestPayoutReturns: nil, + payoutRepoFindLatestPayoutError: errors.New("db error"), + payoutRepoFindLatestPayoutNumOfCalls: 1, + // CalculateNodeStatisticsForInterval + calculateStatisticsFromLastPayoutReturns: nil, + calculateStatisticsFromLastPayoutError: errors.New("db error"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // create mock controller + nodeRepoMock := mocks.NodeRepository{} + nodeRepoMock.On("GetAll").Return( + test.nodeRepoGetAllReturns, test.nodeRepoGetAllError, + ) + recordRepoMock := mocks.RecordRepository{} + recordRepoMock.On("FindSuccessfulRecordsInsideInterval", + test.nodeID, test.intervalStart, test.intervalEnd, + ).Return( + test.recordRepoFindSuccessfulRecordsInsideIntervalReturns, + test.recordRepoFindSuccessfulRecordsInsideIntervalError, + ) + downtimeRepoMock := mocks.DowntimeRepository{} + downtimeRepoMock.On("FindDowntimesInsideInterval", + test.nodeID, test.intervalStart, test.intervalEnd, + ).Return( + test.downtimeRepoFindDowntimesInsideIntervalReturns, + test.downtimeRepoFindDowntimesInsideIntervalError, + ) + pingRepoMock := mocks.PingRepository{} + pingRepoMock.On("CalculateDowntime", + test.nodeID, test.intervalEnd, + ).Return( + time.Now(), + test.pingRepoCalculateDowntimeReturnDuration, + test.pingRepoCalculateDowntimeError, + ) + payoutRepoMock := mocks.PayoutRepository{} + payoutRepoMock.On("FindLatestPayout").Return( + test.payoutRepoFindLatestPayoutReturns, + test.payoutRepoFindLatestPayoutError, + ) + repos := repositories.Repos{ + PingRepo: &pingRepoMock, + RecordRepo: &recordRepoMock, + DowntimeRepo: &downtimeRepoMock, + NodeRepo: &nodeRepoMock, + PayoutRepo: &payoutRepoMock, + } + + statisticsForPayout, err := CalculateStatisticsFromLastPayout(repos) + + assert.Equal(t, test.calculateStatisticsFromLastPayoutError, err) + assert.Equal(t, test.calculateStatisticsFromLastPayoutReturns, statisticsForPayout) + + nodeRepoMock.AssertNumberOfCalls(t, + "GetAll", + test.nodeRepoGetAllNumOfCalls, + ) + recordRepoMock.AssertNumberOfCalls(t, + "FindSuccessfulRecordsInsideInterval", + test.recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls, + ) + downtimeRepoMock.AssertNumberOfCalls(t, + "FindDowntimesInsideInterval", + test.downtimeRepoFindDowntimesInsideIntervalNumOfCalls, + ) + pingRepoMock.AssertNumberOfCalls(t, + "CalculateDowntime", + test.pingRepoCalculateDowntimeNumOfCalls, + ) + }) + } + + nowFunc = time.Now +} + +func Test_CalculateNodeStatisticsForInterval(t *testing.T) { + now := time.Now() + + tests := []struct { + name string + nodeID string + intervalStart time.Time + intervalEnd time.Time + // RecordRepo.FindByNodeID + recordRepoFindSuccessfulRecordsInsideIntervalReturns []models.Record + recordRepoFindSuccessfulRecordsInsideIntervalError error + recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls int + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns []models.Downtime + downtimeRepoFindDowntimesInsideIntervalError error + downtimeRepoFindDowntimesInsideIntervalNumOfCalls int + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration time.Duration + pingRepoCalculateDowntimeError error + pingRepoCalculateDowntimeNumOfCalls int + // CalculateNodeStatisticsForInterval + calculateNodeStatisticsForIntervalReturns *models.NodeStatsDetails + calculateNodeStatisticsForIntervalError error + }{ + { + name: "valid statistics with multiple records and no downtime", + nodeID: "1", + // interval of 24 hours + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // RecordRepo.FindByNodeID + recordRepoFindSuccessfulRecordsInsideIntervalReturns: []models.Record{ + {ID: 1, NodeId: "1", Status: "successful", Timestamp: now.Add(-20 * time.Hour)}, + {ID: 2, NodeId: "1", Status: "successful", Timestamp: now.Add(-18 * time.Hour)}, + {ID: 3, NodeId: "1", Status: "successful", Timestamp: now.Add(-17 * time.Hour)}, + {ID: 4, NodeId: "1", Status: "successful", Timestamp: now.Add(-15 * time.Hour)}, + {ID: 5, NodeId: "1", Status: "successful", Timestamp: now.Add(-12 * time.Hour)}, + }, + recordRepoFindSuccessfulRecordsInsideIntervalError: nil, + recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls: 1, + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns: nil, + downtimeRepoFindDowntimesInsideIntervalError: errors.New("not found"), + downtimeRepoFindDowntimesInsideIntervalNumOfCalls: 1, + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 5 * time.Second, + pingRepoCalculateDowntimeError: nil, + pingRepoCalculateDowntimeNumOfCalls: 1, + // CalculateNodeStatisticsForInterval + calculateNodeStatisticsForIntervalReturns: &models.NodeStatsDetails{ + TotalPings: 17280, // no downtime - max number of pings + TotalRequests: 5, + }, + calculateNodeStatisticsForIntervalError: nil, + }, + { + name: "valid statistics with no records and no downtime", + nodeID: "1", + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // RecordRepo.FindByNodeID + recordRepoFindSuccessfulRecordsInsideIntervalReturns: nil, + recordRepoFindSuccessfulRecordsInsideIntervalError: errors.New("not found"), + recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls: 1, + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns: nil, + downtimeRepoFindDowntimesInsideIntervalError: errors.New("not found"), + downtimeRepoFindDowntimesInsideIntervalNumOfCalls: 1, + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 5 * time.Second, + pingRepoCalculateDowntimeError: nil, + pingRepoCalculateDowntimeNumOfCalls: 1, + // CalculateNodeStatisticsForInterval + calculateNodeStatisticsForIntervalReturns: &models.NodeStatsDetails{ + TotalPings: 17280, // no downtime - max number of pings + TotalRequests: 0, + }, + calculateNodeStatisticsForIntervalError: nil, + }, + { + name: "error on fetching records", + nodeID: "1", + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // RecordRepo.FindByNodeID + recordRepoFindSuccessfulRecordsInsideIntervalReturns: nil, + recordRepoFindSuccessfulRecordsInsideIntervalError: errors.New("db error"), + recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls: 1, + // CalculateNodeStatisticsForInterval + calculateNodeStatisticsForIntervalReturns: nil, + calculateNodeStatisticsForIntervalError: errors.New("db error"), + }, + { + name: "error on fetching pings", + nodeID: "1", + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // RecordRepo.FindByNodeID + recordRepoFindSuccessfulRecordsInsideIntervalReturns: nil, + recordRepoFindSuccessfulRecordsInsideIntervalError: errors.New("not found"), + recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls: 1, + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns: nil, + downtimeRepoFindDowntimesInsideIntervalError: errors.New("db error"), + downtimeRepoFindDowntimesInsideIntervalNumOfCalls: 1, + // CalculateNodeStatisticsForInterval + calculateNodeStatisticsForIntervalReturns: nil, + calculateNodeStatisticsForIntervalError: errors.New("db error"), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // create mock controller + recordRepoMock := mocks.RecordRepository{} + recordRepoMock.On("FindSuccessfulRecordsInsideInterval", + test.nodeID, test.intervalStart, test.intervalEnd, + ).Return( + test.recordRepoFindSuccessfulRecordsInsideIntervalReturns, + test.recordRepoFindSuccessfulRecordsInsideIntervalError, + ) + downtimeRepoMock := mocks.DowntimeRepository{} + downtimeRepoMock.On("FindDowntimesInsideInterval", + test.nodeID, test.intervalStart, test.intervalEnd, + ).Return( + test.downtimeRepoFindDowntimesInsideIntervalReturns, + test.downtimeRepoFindDowntimesInsideIntervalError, + ) + pingRepoMock := mocks.PingRepository{} + pingRepoMock.On("CalculateDowntime", + test.nodeID, test.intervalEnd, + ).Return( + time.Now(), + test.pingRepoCalculateDowntimeReturnDuration, + test.pingRepoCalculateDowntimeError, + ) + repos := repositories.Repos{ + PingRepo: &pingRepoMock, + RecordRepo: &recordRepoMock, + DowntimeRepo: &downtimeRepoMock, + } + + statisticsForPayout, err := CalculateNodeStatisticsForInterval(repos, test.nodeID, test.intervalStart, test.intervalEnd) + + assert.Equal(t, test.calculateNodeStatisticsForIntervalError, err) + assert.Equal(t, test.calculateNodeStatisticsForIntervalReturns, statisticsForPayout) + + recordRepoMock.AssertNumberOfCalls(t, + "FindSuccessfulRecordsInsideInterval", + test.recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls, + ) + downtimeRepoMock.AssertNumberOfCalls(t, + "FindDowntimesInsideInterval", + test.downtimeRepoFindDowntimesInsideIntervalNumOfCalls, + ) + pingRepoMock.AssertNumberOfCalls(t, + "CalculateDowntime", + test.pingRepoCalculateDowntimeNumOfCalls, + ) + }) + } +} + +func Test_CalculateStatisticsForInterval(t *testing.T) { + now := time.Now() + + tests := []struct { + name string + nodeID string + intervalStart time.Time + intervalEnd time.Time + // NodeRepo.GetAll + nodeRepoGetAllReturns *[]models.Node + nodeRepoGetAllError error + nodeRepoGetAllNumOfCalls int + // RecordRepo.FindByNodeID + recordRepoFindSuccessfulRecordsInsideIntervalReturns []models.Record + recordRepoFindSuccessfulRecordsInsideIntervalError error + recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls int + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns []models.Downtime + downtimeRepoFindDowntimesInsideIntervalError error + downtimeRepoFindDowntimesInsideIntervalNumOfCalls int + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration time.Duration + pingRepoCalculateDowntimeError error + pingRepoCalculateDowntimeNumOfCalls int + // CalculateNodeStatisticsForInterval + calculateStatisticsForIntervalReturns map[string]models.NodeStatsDetails + calculateStatisticsForIntervalError error + }{ + { + name: "valid statistics with multiple records and no downtime", + nodeID: "1", + // interval of 24 hours + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // NodeRepo.GetAll + nodeRepoGetAllReturns: &[]models.Node{ + { + ID: "1", + }, + }, + nodeRepoGetAllError: nil, + nodeRepoGetAllNumOfCalls: 1, + // RecordRepo.FindByNodeID + recordRepoFindSuccessfulRecordsInsideIntervalReturns: []models.Record{ + {ID: 1, NodeId: "1", Status: "successful", Timestamp: now.Add(-20 * time.Hour)}, + {ID: 2, NodeId: "1", Status: "successful", Timestamp: now.Add(-18 * time.Hour)}, + {ID: 3, NodeId: "1", Status: "successful", Timestamp: now.Add(-17 * time.Hour)}, + {ID: 4, NodeId: "1", Status: "successful", Timestamp: now.Add(-15 * time.Hour)}, + {ID: 5, NodeId: "1", Status: "successful", Timestamp: now.Add(-12 * time.Hour)}, + }, + recordRepoFindSuccessfulRecordsInsideIntervalError: nil, + recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls: 1, + // DowntimeRepo.FindByNodeID + downtimeRepoFindDowntimesInsideIntervalReturns: nil, + downtimeRepoFindDowntimesInsideIntervalError: errors.New("not found"), + downtimeRepoFindDowntimesInsideIntervalNumOfCalls: 1, + // PingRepo.CalculateDowntime + pingRepoCalculateDowntimeReturnDuration: 5 * time.Second, + pingRepoCalculateDowntimeError: nil, + pingRepoCalculateDowntimeNumOfCalls: 1, + // CalculateNodeStatisticsForInterval + calculateStatisticsForIntervalReturns: map[string]models.NodeStatsDetails{ + "1": { + TotalPings: 17280, // no downtime - max number of pings + TotalRequests: 5, + }, + }, + calculateStatisticsForIntervalError: nil, + }, + { + name: "get all nodes fails", + nodeID: "1", + // interval of 24 hours + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // NodeRepo.GetAll + nodeRepoGetAllReturns: nil, + nodeRepoGetAllError: errors.New("not found"), + nodeRepoGetAllNumOfCalls: 1, + // CalculateNodeStatisticsForInterval + calculateStatisticsForIntervalReturns: nil, + calculateStatisticsForIntervalError: errors.New("not found"), + }, + { + name: "calculating statistics for node fails", + nodeID: "1", + // interval of 24 hours + intervalStart: now.Add(-24 * time.Hour), + intervalEnd: now, + // NodeRepo.GetAll + nodeRepoGetAllReturns: &[]models.Node{ + { + ID: "1", + }, + }, + nodeRepoGetAllError: nil, + nodeRepoGetAllNumOfCalls: 1, + // RecordRepo.FindByNodeID + recordRepoFindSuccessfulRecordsInsideIntervalReturns: nil, + recordRepoFindSuccessfulRecordsInsideIntervalError: errors.New("db error"), + recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls: 1, + // CalculateNodeStatisticsForInterval + calculateStatisticsForIntervalReturns: nil, + calculateStatisticsForIntervalError: errors.New("db error"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // create mock controller + nodeRepoMock := mocks.NodeRepository{} + nodeRepoMock.On("GetAll").Return( + test.nodeRepoGetAllReturns, test.nodeRepoGetAllError, + ) + recordRepoMock := mocks.RecordRepository{} + recordRepoMock.On("FindSuccessfulRecordsInsideInterval", + test.nodeID, test.intervalStart, test.intervalEnd, + ).Return( + test.recordRepoFindSuccessfulRecordsInsideIntervalReturns, + test.recordRepoFindSuccessfulRecordsInsideIntervalError, + ) + downtimeRepoMock := mocks.DowntimeRepository{} + downtimeRepoMock.On("FindDowntimesInsideInterval", + test.nodeID, test.intervalStart, test.intervalEnd, + ).Return( + test.downtimeRepoFindDowntimesInsideIntervalReturns, + test.downtimeRepoFindDowntimesInsideIntervalError, + ) + pingRepoMock := mocks.PingRepository{} + pingRepoMock.On("CalculateDowntime", + test.nodeID, test.intervalEnd, + ).Return( + time.Now(), + test.pingRepoCalculateDowntimeReturnDuration, + test.pingRepoCalculateDowntimeError, + ) + repos := repositories.Repos{ + PingRepo: &pingRepoMock, + RecordRepo: &recordRepoMock, + DowntimeRepo: &downtimeRepoMock, + NodeRepo: &nodeRepoMock, + } + + statisticsForPayout, err := CalculateStatisticsForInterval(repos, test.intervalStart, test.intervalEnd) + + assert.Equal(t, test.calculateStatisticsForIntervalError, err) + assert.Equal(t, test.calculateStatisticsForIntervalReturns, statisticsForPayout) + + nodeRepoMock.AssertNumberOfCalls(t, + "GetAll", + test.nodeRepoGetAllNumOfCalls, + ) + recordRepoMock.AssertNumberOfCalls(t, + "FindSuccessfulRecordsInsideInterval", + test.recordRepoFindSuccessfulRecordsInsideIntervalNumOfCalls, + ) + downtimeRepoMock.AssertNumberOfCalls(t, + "FindDowntimesInsideInterval", + test.downtimeRepoFindDowntimesInsideIntervalNumOfCalls, + ) + pingRepoMock.AssertNumberOfCalls(t, + "CalculateDowntime", + test.pingRepoCalculateDowntimeNumOfCalls, + ) + }) + } +} diff --git a/mocks/repositories/DowntimeRepository.go b/mocks/repositories/DowntimeRepository.go index 9900b537..22de3226 100644 --- a/mocks/repositories/DowntimeRepository.go +++ b/mocks/repositories/DowntimeRepository.go @@ -7,11 +7,36 @@ import ( mock "github.com/stretchr/testify/mock" ) +import time "time" + // DowntimeRepository is an autogenerated mock type for the DowntimeRepository type type DowntimeRepository struct { mock.Mock } +// FindDowntimesInsideInterval provides a mock function with given fields: nodeID, from, to +func (_m *DowntimeRepository) FindDowntimesInsideInterval(nodeID string, from time.Time, to time.Time) ([]models.Downtime, error) { + ret := _m.Called(nodeID, from, to) + + var r0 []models.Downtime + if rf, ok := ret.Get(0).(func(string, time.Time, time.Time) []models.Downtime); ok { + r0 = rf(nodeID, from, to) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.Downtime) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, time.Time, time.Time) error); ok { + r1 = rf(nodeID, from, to) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Save provides a mock function with given fields: downtime func (_m *DowntimeRepository) Save(downtime *models.Downtime) error { ret := _m.Called(downtime) diff --git a/mocks/repositories/PaymentRepository.go b/mocks/repositories/PaymentRepository.go new file mode 100644 index 00000000..290069a7 --- /dev/null +++ b/mocks/repositories/PaymentRepository.go @@ -0,0 +1,25 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" +import models "github.com/NodeFactoryIo/vedran/internal/models" + +// PaymentRepository is an autogenerated mock type for the PaymentRepository type +type PaymentRepository struct { + mock.Mock +} + +// Save provides a mock function with given fields: payment +func (_m *PaymentRepository) Save(payment *models.Payout) error { + ret := _m.Called(payment) + + var r0 error + if rf, ok := ret.Get(0).(func(*models.Payout) error); ok { + r0 = rf(payment) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mocks/repositories/PayoutRepository.go b/mocks/repositories/PayoutRepository.go new file mode 100644 index 00000000..c8e31f83 --- /dev/null +++ b/mocks/repositories/PayoutRepository.go @@ -0,0 +1,71 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" +import models "github.com/NodeFactoryIo/vedran/internal/models" + +// PayoutRepository is an autogenerated mock type for the PayoutRepository type +type PayoutRepository struct { + mock.Mock +} + +// FindLatestPayout provides a mock function with given fields: +func (_m *PayoutRepository) FindLatestPayout() (*models.Payout, error) { + ret := _m.Called() + + var r0 *models.Payout + if rf, ok := ret.Get(0).(func() *models.Payout); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Payout) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetAll provides a mock function with given fields: +func (_m *PayoutRepository) GetAll() (*[]models.Payout, error) { + ret := _m.Called() + + var r0 *[]models.Payout + if rf, ok := ret.Get(0).(func() *[]models.Payout); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*[]models.Payout) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Save provides a mock function with given fields: payment +func (_m *PayoutRepository) Save(payment *models.Payout) error { + ret := _m.Called(payment) + + var r0 error + if rf, ok := ret.Get(0).(func(*models.Payout) error); ok { + r0 = rf(payment) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mocks/repositories/RecordRepository.go b/mocks/repositories/RecordRepository.go index 6506ecad..f2776b89 100644 --- a/mocks/repositories/RecordRepository.go +++ b/mocks/repositories/RecordRepository.go @@ -7,11 +7,36 @@ import ( mock "github.com/stretchr/testify/mock" ) +import time "time" + // RecordRepository is an autogenerated mock type for the RecordRepository type type RecordRepository struct { mock.Mock } +// FindSuccessfulRecordsInsideInterval provides a mock function with given fields: nodeID, from, to +func (_m *RecordRepository) FindSuccessfulRecordsInsideInterval(nodeID string, from time.Time, to time.Time) ([]models.Record, error) { + ret := _m.Called(nodeID, from, to) + + var r0 []models.Record + if rf, ok := ret.Get(0).(func(string, time.Time, time.Time) []models.Record); ok { + r0 = rf(nodeID, from, to) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.Record) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, time.Time, time.Time) error); ok { + r1 = rf(nodeID, from, to) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Save provides a mock function with given fields: record func (_m *RecordRepository) Save(record *models.Record) error { ret := _m.Called(record)