mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
b6a8185957 | |||
c5da5e4622 | |||
a0f69a371f | |||
2f0cb112dd | |||
991e64b961 | |||
affff0c386 |
File diff suppressed because it is too large
Load Diff
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if authorizedUser == nil || authorizedUser.ID != requestedUser.ID {
|
if authorizedUser == nil || authorizedUser.ID != requestedUser.ID && !authorizedUser.IsAdmin {
|
||||||
err := errors.New(conf.ErrUnauthorized)
|
err := errors.New(conf.ErrUnauthorized)
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return authorizedUser, nil
|
return requestedUser, nil
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/duke-git/lancet/v2/datetime"
|
||||||
"github.com/duke-git/lancet/v2/mathutil"
|
"github.com/duke-git/lancet/v2/mathutil"
|
||||||
"github.com/muety/wakapi/config"
|
"github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
@ -60,7 +61,7 @@ func (srv *DurationService) Get(from, to time.Time, user *models.User, filters *
|
|||||||
continue
|
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(
|
dur := time.Duration(mathutil.Min(
|
||||||
int64(d1.Time.T().Sub(latest.Time.T().Add(latest.Duration))),
|
int64(d1.Time.T().Sub(latest.Time.T().Add(latest.Duration))),
|
||||||
int64(HeartbeatDiffThreshold),
|
int64(HeartbeatDiffThreshold),
|
||||||
|
@ -12,6 +12,7 @@ server:
|
|||||||
app:
|
app:
|
||||||
aggregation_time: '02:15'
|
aggregation_time: '02:15'
|
||||||
report_time_weekly: 'fri,18:00'
|
report_time_weekly: 'fri,18:00'
|
||||||
|
heartbeat_max_age: 87600h # 10 years
|
||||||
inactive_days: 7
|
inactive_days: 7
|
||||||
custom_languages:
|
custom_languages:
|
||||||
vue: Vue
|
vue: Vue
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"info": {
|
"info": {
|
||||||
"_postman_id": "43639725-0458-40d7-a4d4-9f55a539a7f7",
|
"_postman_id": "5c0749a5-6ddf-41ea-82f1-140578788bc3",
|
||||||
"name": "Wakapi API Tests",
|
"name": "Wakapi API Tests",
|
||||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
},
|
},
|
||||||
@ -973,10 +973,9 @@
|
|||||||
"listen": "test",
|
"listen": "test",
|
||||||
"script": {
|
"script": {
|
||||||
"exec": [
|
"exec": [
|
||||||
"// 1640995199 Friday, 31 December 2021 11:59:59 PM (Jan 1st in +1, +2)",
|
"pm.test(\"Status code is 201\", function () {",
|
||||||
"// 1641074399 Saturday, 1 January 2022 9:59:59 PM (Jan 1st in +1, +2)",
|
" pm.response.to.have.status(201);",
|
||||||
"// 1641081599 Saturday, 1 January 2022 11:59:59 PM (Jan 2nd in +1, +2)",
|
"});"
|
||||||
""
|
|
||||||
],
|
],
|
||||||
"type": "text/javascript"
|
"type": "text/javascript"
|
||||||
}
|
}
|
||||||
@ -997,7 +996,7 @@
|
|||||||
"header": [],
|
"header": [],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"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": {
|
"options": {
|
||||||
"raw": {
|
"raw": {
|
||||||
"language": "json"
|
"language": "json"
|
||||||
@ -1331,8 +1330,8 @@
|
|||||||
"",
|
"",
|
||||||
"pm.test(\"Correct dates\", function () {",
|
"pm.test(\"Correct dates\", function () {",
|
||||||
" const jsonData = pm.response.json();",
|
" const jsonData = pm.response.json();",
|
||||||
" pm.expect(moment(jsonData.from).unix()).to.gte(moment(pm.variables.get('tsStartOfDayDate')).unix())",
|
" pm.expect(moment(jsonData.from).unix()).to.gte(moment(pm.variables.get('tsStartOfDayIso')).unix())",
|
||||||
" pm.expect(moment(jsonData.to).unix()).to.gte(moment(pm.variables.get('tsEndOfDayDate')).unix())",
|
" pm.expect(moment(jsonData.to).unix()).to.lte(moment(pm.variables.get('tsEndOfDayIso')).unix())",
|
||||||
"});",
|
"});",
|
||||||
""
|
""
|
||||||
],
|
],
|
||||||
@ -1358,7 +1357,7 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"header": [],
|
"header": [],
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{BASE_URL}}/api/summary?from={{tsStartOfDayDate}}&to={{tsEndOfTomorrowDate}}",
|
"raw": "{{BASE_URL}}/api/summary?from={{tsStartOfDayIso}}&to={{tsEndOfDayIso}}",
|
||||||
"host": [
|
"host": [
|
||||||
"{{BASE_URL}}"
|
"{{BASE_URL}}"
|
||||||
],
|
],
|
||||||
@ -1369,11 +1368,11 @@
|
|||||||
"query": [
|
"query": [
|
||||||
{
|
{
|
||||||
"key": "from",
|
"key": "from",
|
||||||
"value": "{{tsStartOfDayDate}}"
|
"value": "{{tsStartOfDayIso}}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "to",
|
"key": "to",
|
||||||
"value": "{{tsEndOfTomorrowDate}}"
|
"value": "{{tsEndOfDayIso}}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -3371,46 +3370,60 @@
|
|||||||
"exec": [
|
"exec": [
|
||||||
"const moment = require('moment')",
|
"const moment = require('moment')",
|
||||||
"",
|
"",
|
||||||
"const now = moment()",
|
"// pretend we're in Berlin, as this is also the time zone configured for the user",
|
||||||
"const startOfDay = moment().startOf('day')",
|
"const userZone = 'Europe/Berlin'",
|
||||||
"const endOfDay = moment().endOf('day')",
|
|
||||||
"const endOfTomorrow = moment().add(1, 'd').endOf('day')",
|
|
||||||
"",
|
"",
|
||||||
"console.log(`Current timestamp is: ${now.format('x') / 1000}`)",
|
"// 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)",
|
||||||
|
" })",
|
||||||
|
"}",
|
||||||
"",
|
"",
|
||||||
|
"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')",
|
||||||
"",
|
"",
|
||||||
"// Auth stuff",
|
" // Auth stuff",
|
||||||
"const readApiKey = pm.variables.get('READUSER_API_KEY')",
|
" const readApiKey = pm.variables.get('READUSER_API_KEY')",
|
||||||
"const writeApiKey = pm.variables.get('WRITEUSER_API_KEY')",
|
" const writeApiKey = pm.variables.get('WRITEUSER_API_KEY')",
|
||||||
"",
|
"",
|
||||||
"if (!readApiKey || !writeApiKey) {",
|
" console.log(readApiKey)",
|
||||||
|
"",
|
||||||
|
" if (!readApiKey || !writeApiKey) {",
|
||||||
" throw new Error('no api key given')",
|
" throw new Error('no api key given')",
|
||||||
"}",
|
" }",
|
||||||
"",
|
"",
|
||||||
"pm.variables.set('READUSER_TOKEN', base64encode(readApiKey))",
|
" pm.variables.set('READUSER_TOKEN', base64encode(readApiKey))",
|
||||||
"pm.variables.set('WRITEUSER_TOKEN', base64encode(writeApiKey))",
|
" pm.variables.set('WRITEUSER_TOKEN', base64encode(writeApiKey))",
|
||||||
"",
|
"",
|
||||||
"function base64encode(str) {",
|
" function base64encode(str) {",
|
||||||
" return Buffer.from(str, 'utf-8').toString('base64')",
|
" return Buffer.from(str, 'utf-8').toString('base64')",
|
||||||
"}",
|
" }",
|
||||||
"",
|
"",
|
||||||
"// Heartbeat stuff",
|
" // Heartbeat stuff",
|
||||||
"pm.variables.set('tsNow', now.format('x') / 1000)",
|
" pm.variables.set('tsNow', now.clone().format('x') / 1000)",
|
||||||
"pm.variables.set('tsNowMinus1Min', now.add(-1, 'm').format('x') / 1000)",
|
" pm.variables.set('tsNowMinus1Min', now.clone().add(-1, 'm').format('x') / 1000)",
|
||||||
"pm.variables.set('tsNowMinus2Min', now.add(-2, 'm').format('x') / 1000)",
|
" pm.variables.set('tsNowMinus2Min', now.clone().add(-2, 'm').format('x') / 1000)",
|
||||||
"pm.variables.set('tsNowMinus3Min', now.add(-3, '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('tsStartOfDay', startOfDay.format('x') / 1000)",
|
||||||
"pm.variables.set('tsEndOfDay', endOfDay.format('x') / 1000)",
|
" pm.variables.set('tsEndOfDay', endOfDay.format('x') / 1000)",
|
||||||
"pm.variables.set('tsEndOfTomorrow', endOfTomorrow.format('x') / 1000)",
|
" pm.variables.set('tsEndOfTomorrow', endOfTomorrow.format('x') / 1000)",
|
||||||
"pm.variables.set('tsStartOfDayIso', startOfDay.toISOString())",
|
" pm.variables.set('tsStartOfDayIso', startOfDay.toISOString())",
|
||||||
"pm.variables.set('tsEndOfDayIso', endOfDay.toISOString())",
|
" pm.variables.set('tsEndOfDayIso', endOfDay.toISOString())",
|
||||||
"pm.variables.set('tsEndOfTomorrowIso', endOfTomorrow.toISOString())",
|
" pm.variables.set('tsEndOfTomorrowIso', endOfTomorrow.toISOString())",
|
||||||
"pm.variables.set('tsStartOfDayDate', startOfDay.format('YYYY-MM-DD'))",
|
" pm.variables.set('ts1', now.clone().startOf('hour').format('x') / 1000)",
|
||||||
"pm.variables.set('tsEndOfDayDate', endOfDay.format('YYYY-MM-DD'))",
|
" pm.variables.set('ts2', now.clone().startOf('hour').add(1, 'm').format('x') / 1000)",
|
||||||
"pm.variables.set('tsEndOfTomorrowDate', endOfTomorrow.format('YYYY-MM-DD'))",
|
" pm.variables.set('ts3', now.clone().startOf('hour').add(2, 'm').format('x') / 1000)",
|
||||||
"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)"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user