Skip to content

Commit a951d83

Browse files
committed
fix: hydrate time entries with the token owner details
1 parent 45bd1e7 commit a951d83

3 files changed

Lines changed: 154 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- `report` and `out` commands no longer fail with "Access Denied" when the user
13+
is not allowed to list all the users of the workspace
14+
1015
## [v0.64.1] - 2026-06-10
1116

1217
### Fixed

api/client.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,11 +451,18 @@ func (c *client) GetUsersHydratedTimeEntries(p GetUserTimeEntriesParam) ([]dto.T
451451
return timeEntries, err
452452
}
453453

454-
user, err := c.GetUser(GetUser{p.Workspace, p.UserID})
454+
user, err := c.GetMe()
455455
if err != nil {
456456
return timeEntries, err
457457
}
458458

459+
if user.ID != p.UserID {
460+
if user, err = c.GetUser(
461+
GetUser{p.Workspace, p.UserID}); err != nil {
462+
return timeEntries, err
463+
}
464+
}
465+
459466
for i := 0; i < len(timeEntries); i++ {
460467
timeEntries[i].User = &user
461468
}

api/timeentry_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,144 @@ func TestCreateTimeEntry(t *testing.T) {
128128
})
129129
}
130130
}
131+
132+
func TestGetUsersHydratedTimeEntries(t *testing.T) {
133+
otherID := "62f2af744a912b05acc7c79f"
134+
meUri := "/v1/user"
135+
entriesUri := func(user string) string {
136+
return "/v1/workspaces/" + exampleID + "/user/" + user +
137+
"/time-entries?hydrated=1&page=1&page-size=50"
138+
}
139+
usersUri := "/v1/workspaces/" + exampleID + "/users?page=1&page-size=50"
140+
141+
me := dto.User{ID: exampleID, Name: "me"}
142+
other := dto.User{ID: otherID, Name: "other"}
143+
meBody := `{"id":"` + exampleID + `","name":"me"}`
144+
145+
tts := []testCase{
146+
(&multiRequestTestCase{
147+
name: "hydrates with the token owner",
148+
param: api.GetUserTimeEntriesParam{
149+
Workspace: exampleID,
150+
UserID: exampleID,
151+
PaginationParam: api.AllPages(),
152+
},
153+
154+
result: []dto.TimeEntry{{ID: "t1", User: &me}},
155+
}).
156+
addHttpCall(&httpRequest{
157+
method: "get",
158+
url: entriesUri(exampleID),
159+
status: 200,
160+
response: `[{"id":"t1"}]`,
161+
}).
162+
addHttpCall(&httpRequest{
163+
method: "get",
164+
url: meUri,
165+
status: 200,
166+
response: meBody,
167+
}),
168+
(&multiRequestTestCase{
169+
name: "hydrates another user from the workspace",
170+
param: api.GetUserTimeEntriesParam{
171+
Workspace: exampleID,
172+
UserID: otherID,
173+
PaginationParam: api.AllPages(),
174+
},
175+
176+
result: []dto.TimeEntry{{ID: "t1", User: &other}},
177+
}).
178+
addHttpCall(&httpRequest{
179+
method: "get",
180+
url: entriesUri(otherID),
181+
status: 200,
182+
response: `[{"id":"t1"}]`,
183+
}).
184+
addHttpCall(&httpRequest{
185+
method: "get",
186+
url: meUri,
187+
status: 200,
188+
response: meBody,
189+
}).
190+
addHttpCall(&httpRequest{
191+
method: "get",
192+
url: usersUri,
193+
status: 200,
194+
response: `[{"id":"` + otherID + `","name":"other"}]`,
195+
}),
196+
(&multiRequestTestCase{
197+
name: "fails when the token owner is unknown",
198+
param: api.GetUserTimeEntriesParam{
199+
Workspace: exampleID,
200+
UserID: exampleID,
201+
PaginationParam: api.AllPages(),
202+
},
203+
204+
err: `Access Denied \(code: 501\)`,
205+
}).
206+
addHttpCall(&httpRequest{
207+
method: "get",
208+
url: entriesUri(exampleID),
209+
status: 200,
210+
response: `[{"id":"t1"}]`,
211+
}).
212+
addHttpCall(&httpRequest{
213+
method: "get",
214+
url: meUri,
215+
status: 403,
216+
response: `{"code": 501, "message":"Access Denied"}`,
217+
}),
218+
(&multiRequestTestCase{
219+
name: "fails when the workspace users can't be listed",
220+
param: api.GetUserTimeEntriesParam{
221+
Workspace: exampleID,
222+
UserID: otherID,
223+
PaginationParam: api.AllPages(),
224+
},
225+
226+
err: `get users.*: Access Denied \(code: 501\)`,
227+
}).
228+
addHttpCall(&httpRequest{
229+
method: "get",
230+
url: entriesUri(otherID),
231+
status: 200,
232+
response: `[{"id":"t1"}]`,
233+
}).
234+
addHttpCall(&httpRequest{
235+
method: "get",
236+
url: meUri,
237+
status: 200,
238+
response: meBody,
239+
}).
240+
addHttpCall(&httpRequest{
241+
method: "get",
242+
url: usersUri,
243+
status: 403,
244+
response: `{"code": 501, "message":"Access Denied"}`,
245+
}),
246+
&simpleTestCase{
247+
name: "fails when the time entries can't be listed",
248+
param: api.GetUserTimeEntriesParam{
249+
Workspace: exampleID,
250+
UserID: exampleID,
251+
PaginationParam: api.AllPages(),
252+
},
253+
254+
requestMethod: "get",
255+
requestUrl: entriesUri(exampleID),
256+
257+
responseStatus: 400,
258+
responseBody: `{"code": 10, "message":"error"}`,
259+
260+
err: `get time entries from user .*: error \(code: 10\)`,
261+
},
262+
}
263+
264+
for _, tt := range tts {
265+
runClient(t, tt,
266+
func(c api.Client, p interface{}) (interface{}, error) {
267+
return c.GetUsersHydratedTimeEntries(
268+
p.(api.GetUserTimeEntriesParam))
269+
})
270+
}
271+
}

0 commit comments

Comments
 (0)