Files
forge/tests/integration/api_issue_tracked_time_test.go
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

206 lines
8.2 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"testing"
"time"
auth_model "github.com/hanzoai/git/models/auth"
issues_model "github.com/hanzoai/git/models/issues"
"github.com/hanzoai/git/models/perm"
repo_model "github.com/hanzoai/git/models/repo"
"github.com/hanzoai/git/models/unittest"
user_model "github.com/hanzoai/git/models/user"
"github.com/hanzoai/git/modules/json"
api "github.com/hanzoai/git/modules/structs"
repo_service "github.com/hanzoai/git/services/repository"
"github.com/hanzoai/git/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIGetTrackedTimes(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
assert.NoError(t, issue2.LoadRepo(t.Context()))
session := loginUser(t, user2.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
req := NewRequestf(t, "GET", "/v1/repos/%s/%s/issues/%d/times", user2.Name, issue2.Repo.Name, issue2.Index).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
apiTimes := DecodeJSON(t, resp, api.TrackedTimeList{})
expect, err := issues_model.GetTrackedTimes(t.Context(), &issues_model.FindTrackedTimesOptions{IssueID: issue2.ID})
assert.NoError(t, err)
assert.Len(t, apiTimes, 3)
for i, time := range expect {
assert.Equal(t, time.ID, apiTimes[i].ID)
assert.Equal(t, issue2.Title, apiTimes[i].Issue.Title)
assert.Equal(t, issue2.ID, apiTimes[i].IssueID)
assert.Equal(t, time.Created.Unix(), apiTimes[i].Created.Unix())
assert.Equal(t, time.Time, apiTimes[i].Time)
user, err := user_model.GetUserByID(t.Context(), time.UserID)
assert.NoError(t, err)
assert.Equal(t, user.Name, apiTimes[i].UserName)
}
// test filter
since := "2000-01-01T00%3A00%3A02%2B00%3A00" // 946684802
before := "2000-01-01T00%3A00%3A12%2B00%3A00" // 946684812
req = NewRequestf(t, "GET", "/v1/repos/%s/%s/issues/%d/times?since=%s&before=%s", user2.Name, issue2.Repo.Name, issue2.Index, since, before).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
filterAPITimes := DecodeJSON(t, resp, api.TrackedTimeList{})
assert.Len(t, filterAPITimes, 2)
assert.Equal(t, int64(3), filterAPITimes[0].ID)
assert.Equal(t, int64(6), filterAPITimes[1].ID)
}
// TestAPIGetTrackedTimesNonExistentUserFilter ensures filtering by a user that
// does not exist returns a clean 404 instead of panicking (nil pointer dereference).
func TestAPIGetTrackedTimesNonExistentUserFilter(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
assert.NoError(t, issue2.LoadRepo(t.Context()))
session := loginUser(t, user2.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadRepository)
for _, tc := range []struct {
name string
url string
}{
{"repository level", fmt.Sprintf("/v1/repos/%s/%s/times?user=nonexistentuser", user2.Name, issue2.Repo.Name)},
{"issue level", fmt.Sprintf("/v1/repos/%s/%s/issues/%d/times?user=nonexistentuser", user2.Name, issue2.Repo.Name, issue2.Index)},
} {
t.Run(tc.name, func(t *testing.T) {
req := NewRequest(t, "GET", tc.url).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusNotFound)
assert.True(t, json.Valid(resp.Body.Bytes()), "response body must be a single JSON value, got: %s", resp.Body.Bytes())
var apiError api.APIError
DecodeJSON(t, resp, &apiError)
assert.Contains(t, apiError.Message, "user does not exist")
})
}
t.Run("existing user", func(t *testing.T) {
req := NewRequestf(t, "GET", "/v1/repos/%s/%s/times?user=%s", user2.Name, issue2.Repo.Name, user2.Name).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, api.TrackedTimeList{})
})
}
func TestAPIUserTrackedTimesOmitsInaccessiblePrivateIssues(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
privateRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
assert.True(t, privateRepo.IsPrivate)
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: privateRepo.ID})
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), privateRepo, user, perm.AccessModeRead))
_, err := issues_model.AddTime(t.Context(), user, issue, 60, time.Time{})
assert.NoError(t, err)
assert.NoError(t, repo_service.DeleteCollaboration(t.Context(), privateRepo, user))
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadUser)
req := NewRequest(t, "GET", "/v1/user/times").AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
trackedTimes := DecodeJSON(t, resp, api.TrackedTimeList{})
for _, trackedTime := range trackedTimes {
if assert.NotNil(t, trackedTime.Issue) {
assert.NotEqual(t, issue.ID, trackedTime.Issue.ID)
}
}
}
func TestAPIDeleteTrackedTime(t *testing.T) {
defer tests.PrepareTestEnv(t)()
time6 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 6})
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
assert.NoError(t, issue2.LoadRepo(t.Context()))
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user2.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
// Deletion not allowed
req := NewRequestf(t, "DELETE", "/v1/repos/%s/%s/issues/%d/times/%d", user2.Name, issue2.Repo.Name, issue2.Index, time6.ID).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
// Deletion should be scoped to the issue in the URL
time5 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 5})
req = NewRequestf(t, "DELETE", "/v1/repos/%s/%s/issues/%d/times/%d", user2.Name, issue2.Repo.Name, issue2.Index, time5.ID).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
time3 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 3})
req = NewRequestf(t, "DELETE", "/v1/repos/%s/%s/issues/%d/times/%d", user2.Name, issue2.Repo.Name, issue2.Index, time3.ID).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
// Delete non existing time
MakeRequest(t, req, http.StatusNotFound)
// Reset time of user 2 on issue 2
trackedSeconds, err := issues_model.GetTrackedSeconds(t.Context(), issues_model.FindTrackedTimesOptions{IssueID: 2, UserID: 2})
assert.NoError(t, err)
assert.Equal(t, int64(3661), trackedSeconds)
req = NewRequestf(t, "DELETE", "/v1/repos/%s/%s/issues/%d/times", user2.Name, issue2.Repo.Name, issue2.Index).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
MakeRequest(t, req, http.StatusNotFound)
trackedSeconds, err = issues_model.GetTrackedSeconds(t.Context(), issues_model.FindTrackedTimesOptions{IssueID: 2, UserID: 2})
assert.NoError(t, err)
assert.Equal(t, int64(0), trackedSeconds)
}
func TestAPIAddTrackedTimes(t *testing.T) {
defer tests.PrepareTestEnv(t)()
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
assert.NoError(t, issue2.LoadRepo(t.Context()))
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
session := loginUser(t, admin.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/v1/repos/%s/%s/issues/%d/times", user2.Name, issue2.Repo.Name, issue2.Index)
req := NewRequestWithJSON(t, "POST", urlStr, &api.AddTimeOption{
Time: 33,
User: user2.Name,
Created: time.Unix(947688818, 0),
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
apiNewTime := DecodeJSON(t, resp, &api.TrackedTime{})
assert.EqualValues(t, 33, apiNewTime.Time)
assert.Equal(t, user2.ID, apiNewTime.UserID)
assert.EqualValues(t, 947688818, apiNewTime.Created.Unix())
// adding time for a nonexistent user must return 404, not panic with a 500
req = NewRequestWithJSON(t, "POST", urlStr, &api.AddTimeOption{
Time: 33,
User: "nonexistentuser",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}