mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
d5a85639b1 | |||
b6a8185957 | |||
c5da5e4622 | |||
a0f69a371f | |||
2f0cb112dd | |||
991e64b961 | |||
affff0c386 |
File diff suppressed because it is too large
Load Diff
@ -109,7 +109,7 @@ func (r *SummaryRepository) populateItems(summaries []*models.Summary, condition
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if _, ok := summaryMap[item.SummaryID]; ok {
|
||||
if _, ok := summaryMap[item.SummaryID]; !ok {
|
||||
continue
|
||||
}
|
||||
l := summaryMap[item.SummaryID][0].ItemsByType(item.Type)
|
||||
|
128
routes/compat/wakatime/v1/users_test.go
Normal file
128
routes/compat/wakatime/v1/users_test.go
Normal file
@ -0,0 +1,128 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
"github.com/muety/wakapi/mocks"
|
||||
"github.com/muety/wakapi/models"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
adminUser = &models.User{
|
||||
ID: "AdminUser",
|
||||
ApiKey: "admin-user-api-key",
|
||||
Email: "admin@user.com",
|
||||
IsAdmin: true,
|
||||
CreatedAt: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 1, time.UTC)),
|
||||
LastLoggedInAt: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 2, time.UTC)),
|
||||
}
|
||||
|
||||
basicUser = &models.User{
|
||||
ID: "BasicUser",
|
||||
ApiKey: "basic-user-api-key",
|
||||
Email: "basic@user.com",
|
||||
IsAdmin: false,
|
||||
CreatedAt: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 3, time.UTC)),
|
||||
LastLoggedInAt: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 4, time.UTC)),
|
||||
}
|
||||
)
|
||||
|
||||
func TestUsersHandler_Get(t *testing.T) {
|
||||
router := mux.NewRouter()
|
||||
apiRouter := router.PathPrefix("/api").Subrouter().StrictSlash(true)
|
||||
apiRouter.Use(middlewares.NewPrincipalMiddleware())
|
||||
|
||||
userServiceMock := new(mocks.UserServiceMock)
|
||||
userServiceMock.On("GetUserById", "AdminUser").Return(adminUser, nil)
|
||||
userServiceMock.On("GetUserByKey", "admin-user-api-key").Return(adminUser, nil)
|
||||
userServiceMock.On("GetUserById", "BasicUser").Return(basicUser, nil)
|
||||
userServiceMock.On("GetUserByKey", "basic-user-api-key").Return(basicUser, nil)
|
||||
|
||||
heartbeatServiceMock := new(mocks.HeartbeatServiceMock)
|
||||
heartbeatServiceMock.On("GetLatestByUser", adminUser).Return(&models.Heartbeat{
|
||||
CreatedAt: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 5, time.UTC)),
|
||||
Time: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 6, time.UTC)),
|
||||
}, nil)
|
||||
heartbeatServiceMock.On("GetLatestByUser", basicUser).Return(&models.Heartbeat{
|
||||
CreatedAt: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 5, time.UTC)),
|
||||
Time: models.CustomTime(time.Date(2022, 2, 2, 22, 22, 22, 6, time.UTC)),
|
||||
}, nil)
|
||||
|
||||
usersHandler := NewUsersHandler(userServiceMock, heartbeatServiceMock)
|
||||
usersHandler.RegisterRoutes(apiRouter)
|
||||
|
||||
t.Run("when requesting own user data", func(t *testing.T) {
|
||||
t.Run("should return own data", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/compat/wakatime/v1/users/AdminUser", nil)
|
||||
req.Header.Add(
|
||||
"Authorization",
|
||||
fmt.Sprintf("Bearer %s", base64.StdEncoding.EncodeToString([]byte(adminUser.ApiKey))),
|
||||
)
|
||||
requestRecorder := httptest.NewRecorder()
|
||||
apiRouter.ServeHTTP(requestRecorder, req)
|
||||
res := requestRecorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Errorf("unextected error. Error: %s", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(data), "\"username\":\"AdminUser\"") {
|
||||
t.Errorf("invalid response received. Expected json Received: %s", string(data))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("when requesting another users data", func(t *testing.T) {
|
||||
t.Run("should respond with '401 unauthorized' if not an admin user", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/compat/wakatime/v1/users/AdminUser", nil)
|
||||
req.Header.Add(
|
||||
"Authorization",
|
||||
fmt.Sprintf("Bearer %s", base64.StdEncoding.EncodeToString([]byte(basicUser.ApiKey))),
|
||||
)
|
||||
requestRecorder := httptest.NewRecorder()
|
||||
apiRouter.ServeHTTP(requestRecorder, req)
|
||||
res := requestRecorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Errorf("unextected error. Error: %s", err)
|
||||
}
|
||||
|
||||
if string(data) != "401 unauthorized" {
|
||||
t.Errorf("invalid response received. Expected: '401 unauthorized' Received: %s", string(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should receive user data if requesting user is an admin", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/compat/wakatime/v1/users/BasicUser", nil)
|
||||
req.Header.Add(
|
||||
"Authorization",
|
||||
fmt.Sprintf("Bearer %s", base64.StdEncoding.EncodeToString([]byte(adminUser.ApiKey))),
|
||||
)
|
||||
requestRecorder := httptest.NewRecorder()
|
||||
apiRouter.ServeHTTP(requestRecorder, req)
|
||||
res := requestRecorder.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Errorf("unextected error. Error: %s", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(data), "\"username\":\"BasicUser\"") {
|
||||
t.Errorf("invalid response received. Expected 'BasicUser' info Received: %s", string(data))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
@ -35,12 +35,12 @@ func CheckEffectiveUser(w http.ResponseWriter, r *http.Request, userService serv
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if authorizedUser == nil || authorizedUser.ID != requestedUser.ID {
|
||||
if authorizedUser == nil || authorizedUser.ID != requestedUser.ID && !authorizedUser.IsAdmin {
|
||||
err := errors.New(conf.ErrUnauthorized)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(err.Error()))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return authorizedUser, nil
|
||||
return requestedUser, nil
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/duke-git/lancet/v2/datetime"
|
||||
"github.com/duke-git/lancet/v2/mathutil"
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
@ -60,7 +61,7 @@ func (srv *DurationService) Get(from, to time.Time, user *models.User, filters *
|
||||
continue
|
||||
}
|
||||
|
||||
sameDay := d1.Time.T().Day() == latest.Time.T().Day()
|
||||
sameDay := datetime.BeginOfDay(d1.Time.T()) == datetime.BeginOfDay(latest.Time.T())
|
||||
dur := time.Duration(mathutil.Min(
|
||||
int64(d1.Time.T().Sub(latest.Time.T().Add(latest.Duration))),
|
||||
int64(HeartbeatDiffThreshold),
|
||||
|
@ -12,6 +12,7 @@ server:
|
||||
app:
|
||||
aggregation_time: '02:15'
|
||||
report_time_weekly: 'fri,18:00'
|
||||
heartbeat_max_age: 87600h # 10 years
|
||||
inactive_days: 7
|
||||
custom_languages:
|
||||
vue: Vue
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "43639725-0458-40d7-a4d4-9f55a539a7f7",
|
||||
"_postman_id": "5c0749a5-6ddf-41ea-82f1-140578788bc3",
|
||||
"name": "Wakapi API Tests",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
@ -973,10 +973,9 @@
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"// 1640995199 Friday, 31 December 2021 11:59:59 PM (Jan 1st in +1, +2)",
|
||||
"// 1641074399 Saturday, 1 January 2022 9:59:59 PM (Jan 1st in +1, +2)",
|
||||
"// 1641081599 Saturday, 1 January 2022 11:59:59 PM (Jan 2nd in +1, +2)",
|
||||
""
|
||||
"pm.test(\"Status code is 201\", function () {",
|
||||
" pm.response.to.have.status(201);",
|
||||
"});"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
@ -997,7 +996,7 @@
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "[{\n \"entity\": \"/home/user1/dev/project1/main.go\",\n \"project\": \"wakapi\",\n \"language\": \"Go\",\n \"is_write\": true,\n \"type\": \"file\",\n \"category\": null,\n \"branch\": null,\n \"time\": 1640995199\n},\n{\n \"entity\": \"/home/user1/dev/project1/main.go\",\n \"project\": \"wakapi\",\n \"language\": \"Go\",\n \"is_write\": true,\n \"type\": \"file\",\n \"category\": null,\n \"branch\": null,\n \"time\": 1641074399\n},\n{\n \"entity\": \"/home/user1/dev/project1/main.go\",\n \"project\": \"wakapi\",\n \"language\": \"Go\",\n \"is_write\": true,\n \"type\": \"file\",\n \"category\": null,\n \"branch\": null,\n \"time\": 1641081599\n}]",
|
||||
"raw": "[{\n \"entity\": \"/home/user1/dev/project1/main.go\",\n \"project\": \"wakapi\",\n \"language\": \"Go\",\n \"is_write\": true,\n \"type\": \"file\",\n \"category\": null,\n \"branch\": null,\n \"time\": 1640995200\n},\n{\n \"entity\": \"/home/user1/dev/project1/main.go\",\n \"project\": \"wakapi\",\n \"language\": \"Go\",\n \"is_write\": true,\n \"type\": \"file\",\n \"category\": null,\n \"branch\": null,\n \"time\": 1641074400\n},\n{\n \"entity\": \"/home/user1/dev/project1/main.go\",\n \"project\": \"wakapi\",\n \"language\": \"Go\",\n \"is_write\": true,\n \"type\": \"file\",\n \"category\": null,\n \"branch\": null,\n \"time\": 1641081600\n}]",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@ -1331,8 +1330,8 @@
|
||||
"",
|
||||
"pm.test(\"Correct dates\", function () {",
|
||||
" const jsonData = pm.response.json();",
|
||||
" pm.expect(moment(jsonData.from).unix()).to.gte(moment(pm.variables.get('tsStartOfDayDate')).unix())",
|
||||
" pm.expect(moment(jsonData.to).unix()).to.gte(moment(pm.variables.get('tsEndOfDayDate')).unix())",
|
||||
" pm.expect(moment(jsonData.from).unix()).to.gte(moment(pm.variables.get('tsStartOfDayIso')).unix())",
|
||||
" pm.expect(moment(jsonData.to).unix()).to.lte(moment(pm.variables.get('tsEndOfDayIso')).unix())",
|
||||
"});",
|
||||
""
|
||||
],
|
||||
@ -1358,7 +1357,7 @@
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{BASE_URL}}/api/summary?from={{tsStartOfDayDate}}&to={{tsEndOfTomorrowDate}}",
|
||||
"raw": "{{BASE_URL}}/api/summary?from={{tsStartOfDayIso}}&to={{tsEndOfDayIso}}",
|
||||
"host": [
|
||||
"{{BASE_URL}}"
|
||||
],
|
||||
@ -1369,11 +1368,11 @@
|
||||
"query": [
|
||||
{
|
||||
"key": "from",
|
||||
"value": "{{tsStartOfDayDate}}"
|
||||
"value": "{{tsStartOfDayIso}}"
|
||||
},
|
||||
{
|
||||
"key": "to",
|
||||
"value": "{{tsEndOfTomorrowDate}}"
|
||||
"value": "{{tsEndOfDayIso}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -3371,46 +3370,60 @@
|
||||
"exec": [
|
||||
"const moment = require('moment')",
|
||||
"",
|
||||
"const now = moment()",
|
||||
"const startOfDay = moment().startOf('day')",
|
||||
"const endOfDay = moment().endOf('day')",
|
||||
"const endOfTomorrow = moment().add(1, 'd').endOf('day')",
|
||||
"// pretend we're in Berlin, as this is also the time zone configured for the user",
|
||||
"const userZone = 'Europe/Berlin'",
|
||||
"",
|
||||
"console.log(`Current timestamp is: ${now.format('x') / 1000}`)",
|
||||
"",
|
||||
"",
|
||||
"// Auth stuff",
|
||||
"const readApiKey = pm.variables.get('READUSER_API_KEY')",
|
||||
"const writeApiKey = pm.variables.get('WRITEUSER_API_KEY')",
|
||||
"",
|
||||
"if (!readApiKey || !writeApiKey) {",
|
||||
" throw new Error('no api key given')",
|
||||
"// postman doesn't have moment-timezone package included",
|
||||
"// and we can't just use utcOffset(2), because of summer / winter time",
|
||||
"// inspired by https://stackoverflow.com/a/56853085/3112139",
|
||||
"function getUtcOffset(cb) {",
|
||||
" let offset = pm.globals.get('utcOffset')",
|
||||
" if (offset) return cb(offset)",
|
||||
" pm.sendRequest(`https://worldtimeapi.org/api/timezone/${userZone}`, (err, res) => {",
|
||||
" offset = res.json().utc_offset",
|
||||
" pm.globals.set('utcOffset', offset)",
|
||||
" return cb(offset)",
|
||||
" })",
|
||||
"}",
|
||||
"",
|
||||
"pm.variables.set('READUSER_TOKEN', base64encode(readApiKey))",
|
||||
"pm.variables.set('WRITEUSER_TOKEN', base64encode(writeApiKey))",
|
||||
"getUtcOffset((utcOffset) => {",
|
||||
" const now = moment().utcOffset(utcOffset)",
|
||||
" const startOfDay = now.clone().startOf('day')",
|
||||
" const endOfDay = now.clone().endOf('day')",
|
||||
" const endOfTomorrow = now.clone().add(1, 'd').endOf('day')",
|
||||
"",
|
||||
"function base64encode(str) {",
|
||||
" return Buffer.from(str, 'utf-8').toString('base64')",
|
||||
"}",
|
||||
" // Auth stuff",
|
||||
" const readApiKey = pm.variables.get('READUSER_API_KEY')",
|
||||
" const writeApiKey = pm.variables.get('WRITEUSER_API_KEY')",
|
||||
"",
|
||||
"// Heartbeat stuff",
|
||||
"pm.variables.set('tsNow', now.format('x') / 1000)",
|
||||
"pm.variables.set('tsNowMinus1Min', now.add(-1, 'm').format('x') / 1000)",
|
||||
"pm.variables.set('tsNowMinus2Min', now.add(-2, 'm').format('x') / 1000)",
|
||||
"pm.variables.set('tsNowMinus3Min', now.add(-3, 'm').format('x') / 1000)",
|
||||
"pm.variables.set('tsStartOfDay', startOfDay.format('x') / 1000)",
|
||||
"pm.variables.set('tsEndOfDay', endOfDay.format('x') / 1000)",
|
||||
"pm.variables.set('tsEndOfTomorrow', endOfTomorrow.format('x') / 1000)",
|
||||
"pm.variables.set('tsStartOfDayIso', startOfDay.toISOString())",
|
||||
"pm.variables.set('tsEndOfDayIso', endOfDay.toISOString())",
|
||||
"pm.variables.set('tsEndOfTomorrowIso', endOfTomorrow.toISOString())",
|
||||
"pm.variables.set('tsStartOfDayDate', startOfDay.format('YYYY-MM-DD'))",
|
||||
"pm.variables.set('tsEndOfDayDate', endOfDay.format('YYYY-MM-DD'))",
|
||||
"pm.variables.set('tsEndOfTomorrowDate', endOfTomorrow.format('YYYY-MM-DD'))",
|
||||
"pm.variables.set('ts1', now.startOf('hour').format('x') / 1000)",
|
||||
"pm.variables.set('ts2', now.startOf('hour').add(1, 'm').format('x') / 1000)",
|
||||
"pm.variables.set('ts3', now.startOf('hour').add(2, 'm').format('x') / 1000)"
|
||||
" console.log(readApiKey)",
|
||||
"",
|
||||
" if (!readApiKey || !writeApiKey) {",
|
||||
" throw new Error('no api key given')",
|
||||
" }",
|
||||
"",
|
||||
" pm.variables.set('READUSER_TOKEN', base64encode(readApiKey))",
|
||||
" pm.variables.set('WRITEUSER_TOKEN', base64encode(writeApiKey))",
|
||||
"",
|
||||
" function base64encode(str) {",
|
||||
" return Buffer.from(str, 'utf-8').toString('base64')",
|
||||
" }",
|
||||
"",
|
||||
" // Heartbeat stuff",
|
||||
" pm.variables.set('tsNow', now.clone().format('x') / 1000)",
|
||||
" pm.variables.set('tsNowMinus1Min', now.clone().add(-1, 'm').format('x') / 1000)",
|
||||
" pm.variables.set('tsNowMinus2Min', now.clone().add(-2, 'm').format('x') / 1000)",
|
||||
" pm.variables.set('tsNowMinus3Min', now.clone().add(-3, 'm').format('x') / 1000)",
|
||||
" pm.variables.set('tsStartOfDay', startOfDay.format('x') / 1000)",
|
||||
" pm.variables.set('tsEndOfDay', endOfDay.format('x') / 1000)",
|
||||
" pm.variables.set('tsEndOfTomorrow', endOfTomorrow.format('x') / 1000)",
|
||||
" pm.variables.set('tsStartOfDayIso', startOfDay.toISOString())",
|
||||
" pm.variables.set('tsEndOfDayIso', endOfDay.toISOString())",
|
||||
" pm.variables.set('tsEndOfTomorrowIso', endOfTomorrow.toISOString())",
|
||||
" pm.variables.set('ts1', now.clone().startOf('hour').format('x') / 1000)",
|
||||
" pm.variables.set('ts2', now.clone().startOf('hour').add(1, 'm').format('x') / 1000)",
|
||||
" pm.variables.set('ts3', now.clone().startOf('hour').add(2, 'm').format('x') / 1000)",
|
||||
"})"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
Reference in New Issue
Block a user