mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
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]>
206 lines
8.2 KiB
Go
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)
|
|
}
|