From 5ad87616c9c654fc44c611ccfd4e496257c8f96b Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 20 Feb 2026 13:48:54 -0800 Subject: [PATCH] Fix track time issue id (#36664) --- models/issues/comment.go | 2 +- models/issues/tracked_time.go | 6 ++-- routers/api/v1/repo/issue_tracked_time.go | 2 +- routers/web/repo/issue_timetrack.go | 2 +- .../api_issue_tracked_time_test.go | 6 ++++ tests/integration/issue_timetrack_test.go | 32 +++++++++++++++++++ 6 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 tests/integration/issue_timetrack_test.go diff --git a/models/issues/comment.go b/models/issues/comment.go index f15618bf500..25e74c01eab 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -699,7 +699,7 @@ func (c *Comment) LoadTime(ctx context.Context) error { return nil } var err error - c.Time, err = GetTrackedTimeByID(ctx, c.TimeID) + c.Time, err = GetTrackedTimeByID(ctx, c.IssueID, c.TimeID) return err } diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go index 9c11881e442..0b5c341f1f2 100644 --- a/models/issues/tracked_time.go +++ b/models/issues/tracked_time.go @@ -311,13 +311,13 @@ func deleteTime(ctx context.Context, t *TrackedTime) error { } // GetTrackedTimeByID returns raw TrackedTime without loading attributes by id -func GetTrackedTimeByID(ctx context.Context, id int64) (*TrackedTime, error) { +func GetTrackedTimeByID(ctx context.Context, issueID, trackedTimeID int64) (*TrackedTime, error) { time := new(TrackedTime) - has, err := db.GetEngine(ctx).ID(id).Get(time) + has, err := db.GetEngine(ctx).ID(trackedTimeID).Where("issue_id = ?", issueID).Get(time) if err != nil { return nil, err } else if !has { - return nil, db.ErrNotExist{Resource: "tracked_time", ID: id} + return nil, db.ErrNotExist{Resource: "tracked_time", ID: trackedTimeID} } return time, nil } diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index 171da272ccc..7c1e77ccf5c 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -356,7 +356,7 @@ func DeleteTime(ctx *context.APIContext) { return } - time, err := issues_model.GetTrackedTimeByID(ctx, ctx.PathParamInt64("id")) + time, err := issues_model.GetTrackedTimeByID(ctx, issue.ID, ctx.PathParamInt64("id")) if err != nil { if db.IsErrNotExist(err) { ctx.APIErrorNotFound(err) diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go index 985bfd6698f..b9ed059fde9 100644 --- a/routers/web/repo/issue_timetrack.go +++ b/routers/web/repo/issue_timetrack.go @@ -60,7 +60,7 @@ func DeleteTime(c *context.Context) { return } - t, err := issues_model.GetTrackedTimeByID(c, c.PathParamInt64("timeid")) + t, err := issues_model.GetTrackedTimeByID(c, issue.ID, c.PathParamInt64("timeid")) if err != nil { if db.IsErrNotExist(err) { c.NotFound(err) diff --git a/tests/integration/api_issue_tracked_time_test.go b/tests/integration/api_issue_tracked_time_test.go index 7d6992c3279..12f4def9e1a 100644 --- a/tests/integration/api_issue_tracked_time_test.go +++ b/tests/integration/api_issue_tracked_time_test.go @@ -79,6 +79,12 @@ func TestAPIDeleteTrackedTime(t *testing.T) { 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", "/api/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", "/api/v1/repos/%s/%s/issues/%d/times/%d", user2.Name, issue2.Repo.Name, issue2.Index, time3.ID). AddTokenAuth(token) diff --git a/tests/integration/issue_timetrack_test.go b/tests/integration/issue_timetrack_test.go new file mode 100644 index 00000000000..0a3188fca01 --- /dev/null +++ b/tests/integration/issue_timetrack_test.go @@ -0,0 +1,32 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestIssueTimeDeleteScoped(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + assert.NoError(t, issue1.LoadRepo(t.Context())) + tracked := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 5}) + + session := loginUser(t, issue1.Repo.OwnerName) + url := fmt.Sprintf("/%s/%s/issues/%d/times/%d/delete", issue1.Repo.OwnerName, issue1.Repo.Name, issue1.Index, tracked.ID) + req := NewRequestWithValues(t, "POST", url, map[string]string{}) + session.MakeRequest(t, req, http.StatusNotFound) + + tracked = unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: tracked.ID}) + assert.False(t, tracked.Deleted) +}