diff --git a/mocks/summary_service.go b/mocks/summary_service.go new file mode 100644 index 0000000..a238281 --- /dev/null +++ b/mocks/summary_service.go @@ -0,0 +1,47 @@ +package mocks + +import ( + "github.com/muety/wakapi/models" + "github.com/muety/wakapi/models/types" + "github.com/stretchr/testify/mock" + "time" +) + +type SummaryServiceMock struct { + mock.Mock +} + +func (m *SummaryServiceMock) Aliased(t time.Time, t2 time.Time, u *models.User, r types.SummaryRetriever, f *models.Filters, b bool) (*models.Summary, error) { + args := m.Called(t, t2, u, r, f) + return args.Get(0).(*models.Summary), args.Error(1) +} + +func (m *SummaryServiceMock) Retrieve(t time.Time, t2 time.Time, u *models.User, f *models.Filters) (*models.Summary, error) { + args := m.Called(t, t2, u, f) + return args.Get(0).(*models.Summary), args.Error(1) +} + +func (m *SummaryServiceMock) Summarize(t time.Time, t2 time.Time, u *models.User, f *models.Filters) (*models.Summary, error) { + args := m.Called(t, t2, u, f) + return args.Get(0).(*models.Summary), args.Error(1) +} + +func (m *SummaryServiceMock) GetLatestByUser() ([]*models.TimeByUser, error) { + args := m.Called() + return args.Get(0).([]*models.TimeByUser), args.Error(1) +} + +func (m *SummaryServiceMock) DeleteByUser(s string) error { + args := m.Called(s) + return args.Error(0) +} + +func (m *SummaryServiceMock) DeleteByUserBefore(s string, t time.Time) error { + args := m.Called(s, t) + return args.Error(0) +} + +func (m *SummaryServiceMock) Insert(s *models.Summary) error { + args := m.Called(s) + return args.Error(0) +} diff --git a/models/types/types.go b/models/types/types.go new file mode 100644 index 0000000..c88ec00 --- /dev/null +++ b/models/types/types.go @@ -0,0 +1,8 @@ +package types + +import ( + "github.com/muety/wakapi/models" + "time" +) + +type SummaryRetriever func(f, t time.Time, u *models.User, filters *models.Filters) (*models.Summary, error) diff --git a/routes/api/badge.go b/routes/api/badge.go index 29d3944..0c6a45c 100644 --- a/routes/api/badge.go +++ b/routes/api/badge.go @@ -34,7 +34,7 @@ func NewBadgeHandler(userService services.IUserService, summaryService services. } func (h *BadgeHandler) RegisterRoutes(router chi.Router) { - router.Get("/badge/{user}", h.Get) + router.Get("/badge/{user}/*", h.Get) } func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) { diff --git a/routes/api/badge_test.go b/routes/api/badge_test.go new file mode 100644 index 0000000..d4a5a0a --- /dev/null +++ b/routes/api/badge_test.go @@ -0,0 +1,154 @@ +package api + +import ( + "github.com/go-chi/chi/v5" + "github.com/muety/wakapi/middlewares" + "github.com/muety/wakapi/mocks" + "github.com/muety/wakapi/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "io/ioutil" + "net/http" + "net/http/httptest" + "regexp" + "strings" + "testing" + "time" +) + +var ( + user1 = models.User{ + ID: "user1", + ShareDataMaxDays: 30, + ShareLanguages: true, + } + + summary1 = models.Summary{ + User: &user1, + UserID: "user1", + FromTime: models.CustomTime(time.Date(2023, 3, 14, 0, 0, 0, 0, time.Local)), + ToTime: models.CustomTime(time.Date(2023, 3, 14, 23, 59, 59, 0, time.Local)), + Languages: []*models.SummaryItem{ + { + Type: models.SummaryLanguage, + Key: "go", + Total: 12 * time.Minute / time.Second, + }, + }, + } +) + +func TestBadgeHandler_Get(t *testing.T) { + router := chi.NewRouter() + apiRouter := chi.NewRouter() + apiRouter.Use(middlewares.NewPrincipalMiddleware()) + router.Mount("/api", apiRouter) + + userServiceMock := new(mocks.UserServiceMock) + userServiceMock.On("GetUserById", "user1").Return(&user1, nil) + + summaryServiceMock := new(mocks.SummaryServiceMock) + summaryServiceMock.On("Aliased", mock.AnythingOfType("time.Time"), mock.AnythingOfType("time.Time"), &user1, mock.Anything, mock.Anything).Return(&summary1, nil) + + badgeHandler := NewBadgeHandler(userServiceMock, summaryServiceMock) + badgeHandler.RegisterRoutes(apiRouter) + + t.Run("when requesting badge", func(t *testing.T) { + t.Run("should return badge", func(t *testing.T) { + rec := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodGet, "/api/badge/{user}/interval:week/language:go", nil) + req = withUrlParam(req, "user", "user1") + + router.ServeHTTP(rec, req) + res := rec.Result() + defer res.Body.Close() + + assert.Equal(t, http.StatusOK, res.StatusCode) + + data, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Errorf("unextected error. Error: %s", err) + } + + assert.True(t, strings.HasPrefix(string(data), " 2 { + key, val = groups[1], groups[2] + } + assert.Equal(t, tc.key, key) + assert.Equal(t, tc.val, val) + } +} diff --git a/routes/api/test_utils_test.go b/routes/api/test_utils_test.go new file mode 100644 index 0000000..e162d6d --- /dev/null +++ b/routes/api/test_utils_test.go @@ -0,0 +1,17 @@ +package api + +import ( + "context" + "github.com/go-chi/chi/v5" + "net/http" + "strings" +) + +func withUrlParam(r *http.Request, key, value string) *http.Request { + r.URL.RawPath = strings.Replace(r.URL.RawPath, "{"+key+"}", value, 1) + r.URL.Path = strings.Replace(r.URL.Path, "{"+key+"}", value, 1) + rctx := chi.NewRouteContext() + rctx.URLParams.Add(key, value) + r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx)) + return r +} diff --git a/routes/compat/shields/v1/badge.go b/routes/compat/shields/v1/badge.go index ac9f1ef..906406e 100644 --- a/routes/compat/shields/v1/badge.go +++ b/routes/compat/shields/v1/badge.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/go-chi/chi/v5" "github.com/muety/wakapi/helpers" + "github.com/muety/wakapi/models/types" routeutils "github.com/muety/wakapi/routes/utils" "net/http" "time" @@ -97,7 +98,7 @@ func (h *BadgeHandler) loadUserSummary(user *models.User, interval *models.Inter User: user, } - var retrieveSummary services.SummaryRetriever = h.summarySrvc.Retrieve + var retrieveSummary types.SummaryRetriever = h.summarySrvc.Retrieve if summaryParams.Recompute { retrieveSummary = h.summarySrvc.Summarize } diff --git a/routes/compat/shields/v1/badge_test.go b/routes/compat/shields/v1/badge_test.go deleted file mode 100644 index 88292df..0000000 --- a/routes/compat/shields/v1/badge_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package v1 - -import ( - "github.com/stretchr/testify/assert" - "regexp" - "testing" -) - -func TestBadgeHandler_EntityPattern(t *testing.T) { - type test struct { - test string - key string - val string - } - - pathPrefix := "/compat/shields/v1/current/today/" - - tests := []test{ - {test: pathPrefix + "project:wakapi", key: "project", val: "wakapi"}, - {test: pathPrefix + "os:Linux", key: "os", val: "Linux"}, - {test: pathPrefix + "editor:VSCode", key: "editor", val: "VSCode"}, - {test: pathPrefix + "language:Java", key: "language", val: "Java"}, - {test: pathPrefix + "machine:devmachine", key: "machine", val: "devmachine"}, - {test: pathPrefix + "label:work", key: "label", val: "work"}, - {test: pathPrefix + "foo:bar", key: "", val: ""}, // invalid entity - {test: pathPrefix + "project:01234", key: "project", val: "01234"}, // digits only - {test: pathPrefix + "project:anchr-web-ext", key: "project", val: "anchr-web-ext"}, // with dashes - {test: pathPrefix + "project:wakapi v2", key: "project", val: "wakapi v2"}, // with blank space - {test: pathPrefix + "project:project", key: "project", val: "project"}, - {test: pathPrefix + "project:Anchr-Android_v2.0", key: "project", val: "Anchr-Android_v2.0"}, // all the way - } - - sut := regexp.MustCompile(`(project|os|editor|language|machine|label):([^:?&/]+)`) // see entityFilterPattern in badge_utils.go - - for _, tc := range tests { - var key, val string - if groups := sut.FindStringSubmatch(tc.test); len(groups) > 2 { - key, val = groups[1], groups[2] - } - assert.Equal(t, tc.key, key) - assert.Equal(t, tc.val, val) - } -} diff --git a/routes/compat/wakatime/v1/all_time.go b/routes/compat/wakatime/v1/all_time.go index edced1b..3af19eb 100644 --- a/routes/compat/wakatime/v1/all_time.go +++ b/routes/compat/wakatime/v1/all_time.go @@ -7,6 +7,7 @@ import ( "github.com/muety/wakapi/middlewares" "github.com/muety/wakapi/models" v1 "github.com/muety/wakapi/models/compat/wakatime/v1" + "github.com/muety/wakapi/models/types" routeutils "github.com/muety/wakapi/routes/utils" "github.com/muety/wakapi/services" "net/http" @@ -68,7 +69,7 @@ func (h *AllTimeHandler) loadUserSummary(user *models.User, filters *models.Filt Recompute: false, } - var retrieveSummary services.SummaryRetriever = h.summarySrvc.Retrieve + var retrieveSummary types.SummaryRetriever = h.summarySrvc.Retrieve if summaryParams.Recompute { retrieveSummary = h.summarySrvc.Summarize } diff --git a/routes/compat/wakatime/v1/statusbar.go b/routes/compat/wakatime/v1/statusbar.go index 8350d50..cf6201f 100644 --- a/routes/compat/wakatime/v1/statusbar.go +++ b/routes/compat/wakatime/v1/statusbar.go @@ -3,6 +3,7 @@ package v1 import ( "github.com/go-chi/chi/v5" "github.com/muety/wakapi/helpers" + "github.com/muety/wakapi/models/types" "net/http" "time" @@ -90,7 +91,7 @@ func (h *StatusBarHandler) loadUserSummary(user *models.User, start, end time.Ti Recompute: false, } - var retrieveSummary services.SummaryRetriever = h.summarySrvc.Retrieve + var retrieveSummary types.SummaryRetriever = h.summarySrvc.Retrieve if summaryParams.Recompute { retrieveSummary = h.summarySrvc.Summarize } diff --git a/routes/compat/wakatime/v1/users_test.go b/routes/compat/wakatime/v1/users_test.go index 72cc3f9..f1a0a55 100644 --- a/routes/compat/wakatime/v1/users_test.go +++ b/routes/compat/wakatime/v1/users_test.go @@ -77,7 +77,7 @@ func TestUsersHandler_Get(t *testing.T) { data, err := ioutil.ReadAll(res.Body) if err != nil { - t.Errorf("unextected error. Error: %s", err) + t.Errorf("unexpected error. Error: %s", err) } if !strings.Contains(string(data), "\"username\":\"AdminUser\"") { diff --git a/routes/utils/summary_utils.go b/routes/utils/summary_utils.go index 9afcea1..a790fbb 100644 --- a/routes/utils/summary_utils.go +++ b/routes/utils/summary_utils.go @@ -3,6 +3,7 @@ package utils import ( "github.com/muety/wakapi/helpers" "github.com/muety/wakapi/models" + "github.com/muety/wakapi/models/types" "github.com/muety/wakapi/services" "net/http" "strings" @@ -17,7 +18,7 @@ func LoadUserSummary(ss services.ISummaryService, r *http.Request) (*models.Summ } func LoadUserSummaryByParams(ss services.ISummaryService, params *models.SummaryParams) (*models.Summary, error, int) { - var retrieveSummary services.SummaryRetriever = ss.Retrieve + var retrieveSummary types.SummaryRetriever = ss.Retrieve if params.Recompute { retrieveSummary = ss.Summarize } diff --git a/services/services.go b/services/services.go index 969e52e..4b43e0d 100644 --- a/services/services.go +++ b/services/services.go @@ -3,6 +3,7 @@ package services import ( datastructure "github.com/duke-git/lancet/v2/datastructure/set" "github.com/muety/wakapi/models" + "github.com/muety/wakapi/models/types" "github.com/muety/wakapi/utils" "time" ) @@ -88,7 +89,7 @@ type IDurationService interface { } type ISummaryService interface { - Aliased(time.Time, time.Time, *models.User, SummaryRetriever, *models.Filters, bool) (*models.Summary, error) + Aliased(time.Time, time.Time, *models.User, types.SummaryRetriever, *models.Filters, bool) (*models.Summary, error) Retrieve(time.Time, time.Time, *models.User, *models.Filters) (*models.Summary, error) Summarize(time.Time, time.Time, *models.User, *models.Filters) (*models.Summary, error) GetLatestByUser() ([]*models.TimeByUser, error) diff --git a/services/summary.go b/services/summary.go index d302e1d..f571dc2 100644 --- a/services/summary.go +++ b/services/summary.go @@ -7,6 +7,7 @@ import ( "github.com/leandro-lugaresi/hub" "github.com/muety/wakapi/config" "github.com/muety/wakapi/models" + "github.com/muety/wakapi/models/types" "github.com/muety/wakapi/repositories" "github.com/patrickmn/go-cache" "sort" @@ -24,8 +25,6 @@ type SummaryService struct { projectLabelService IProjectLabelService } -type SummaryRetriever func(f, t time.Time, u *models.User, filters *models.Filters) (*models.Summary, error) - func NewSummaryService(summaryRepo repositories.ISummaryRepository, durationService IDurationService, aliasService IAliasService, projectLabelService IProjectLabelService) *SummaryService { srv := &SummaryService{ config: config.Get(), @@ -50,7 +49,7 @@ func NewSummaryService(summaryRepo repositories.ISummaryRepository, durationServ // Public summary generation methods // Aliased retrieves or computes a new summary based on the given SummaryRetriever and augments it with entity aliases and project labels -func (srv *SummaryService) Aliased(from, to time.Time, user *models.User, f SummaryRetriever, filters *models.Filters, skipCache bool) (*models.Summary, error) { +func (srv *SummaryService) Aliased(from, to time.Time, user *models.User, f types.SummaryRetriever, filters *models.Filters, skipCache bool) (*models.Summary, error) { // Check cache cacheKey := srv.getHash(from.String(), to.String(), user.ID, filters.Hash(), "--aliased") if cacheResult, ok := srv.cache.Get(cacheKey); ok && !skipCache {