From a8e1057f5e75b0f8cf11191acfde161aa9d88bd2 Mon Sep 17 00:00:00 2001 From: Shudhanshu Singh Date: Wed, 21 Oct 2026 16:22:33 +0530 Subject: [PATCH] fix(api): keep milestone due date when it is omitted in edit request (#39869) `PATCH /repos/{owner}/{repo}/milestones/{id}` always overwrote the due date with `due_on`, so a request that only changed other fields, such as `{"state": "closed"}`, silently removed it. The due date is now kept when `due_on` is omitted, and `"due_on": null` or `"due_on": ""` remove it. --------- Co-authored-by: wxiaoguang --- models/issues/issue_update.go | 6 +-- modules/optional/serialization.go | 15 +++++- modules/optional/serialization_test.go | 35 ++++++++++++++ modules/structs/issue.go | 16 ++++--- modules/structs/issue_milestone.go | 11 +++-- modules/structs/pull.go | 7 ++- routers/api/v1/repo/issue.go | 48 +++++++++++-------- routers/api/v1/repo/milestone.go | 20 +++++--- routers/api/v1/repo/pull.go | 21 ++++---- routers/common/deadline.go | 34 +++++++++++-- templates/swagger/v1-openapi3.generated.json | 12 ++--- templates/swagger/v1-swagger.generated.json | 12 ++--- tests/integration/api_issue_milestone_test.go | 5 ++ tests/integration/api_issue_test.go | 3 +- tests/integration/api_pull_test.go | 21 ++++++-- 15 files changed, 192 insertions(+), 74 deletions(-) diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 1fd9d0d68f1..f72841ce20c 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -529,7 +529,7 @@ func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_mo // UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it. func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) { - // if the deadline hasn't changed do nothing + // if the deadline hasn't changed, do nothing if issue.DeadlineUnix == deadlineUnix { return nil } @@ -539,11 +539,11 @@ func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeuti if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil { return err } - // Make the comment if _, err = createDeadlineComment(ctx, doer, issue, deadlineUnix); err != nil { - return fmt.Errorf("createRemovedDueDateComment: %w", err) + return fmt.Errorf("createDeadlineComment: %w", err) } + issue.DeadlineUnix = deadlineUnix return nil }) } diff --git a/modules/optional/serialization.go b/modules/optional/serialization.go index a3db68e76dc..409117c62f3 100644 --- a/modules/optional/serialization.go +++ b/modules/optional/serialization.go @@ -4,17 +4,28 @@ package optional import ( + "reflect" + "gitea.dev/modules/json" "go.yaml.in/yaml/v4" ) +func (o *Option[T]) assignUnmarshalledValue(v *T) { + if reflect.TypeFor[T]().Kind() == reflect.Pointer && v == nil { + var zero T + *o = Some[T](zero) + } else { + *o = FromPtr(v) + } +} + func (o *Option[T]) UnmarshalJSON(data []byte) error { var v *T if err := json.Unmarshal(data, &v); err != nil { return err } - *o = FromPtr(v) + o.assignUnmarshalledValue(v) return nil } @@ -31,7 +42,7 @@ func (o *Option[T]) UnmarshalYAML(value *yaml.Node) error { if err := value.Decode(&v); err != nil { return err } - *o = FromPtr(v) + o.assignUnmarshalledValue(v) return nil } diff --git a/modules/optional/serialization_test.go b/modules/optional/serialization_test.go index 966e284e8b3..e65c53a5dbe 100644 --- a/modules/optional/serialization_test.go +++ b/modules/optional/serialization_test.go @@ -6,6 +6,7 @@ package optional_test import ( std_json "encoding/json" //nolint:depguard // for testing purpose "testing" + "time" "gitea.dev/modules/json" "gitea.dev/modules/optional" @@ -28,6 +29,14 @@ type testSerializationStruct struct { OptTwoString optional.Option[string] `json:"optional_two_string" yaml:"optional_two_string"` } +type testSerializationNullValue struct { + OptString optional.Option[string] `json:"opt_null,omitempty" yaml:"opt_null,omitempty"` +} + +type testSerializationNullPointer struct { + OptNull optional.Option[*time.Time] `json:"opt_null,omitempty" yaml:"opt_null,omitempty"` +} + func TestOptionalToJson(t *testing.T) { tests := []struct { name string @@ -63,6 +72,32 @@ func TestOptionalToJson(t *testing.T) { assert.Equal(t, tc.want, string(b), "std json module returned unexpected") }) } + + t.Run("NullValue", func(t *testing.T) { + var v1 testSerializationNullValue + err := json.Unmarshal([]byte(`{"opt_null":null}`), &v1) + assert.NoError(t, err) + assert.False(t, v1.OptString.Has()) + }) + + t.Run("NullPointer", func(t *testing.T) { + var v1 testSerializationNullPointer + err := json.Unmarshal([]byte(`{"opt_null":null}`), &v1) + assert.NoError(t, err) + assert.True(t, v1.OptNull.Has()) + assert.Nil(t, v1.OptNull.Value()) + + var v2 testSerializationNullPointer + err = std_json.Unmarshal([]byte(`{}`), &v2) + assert.NoError(t, err) + assert.False(t, v2.OptNull.Has()) + + var v3 testSerializationNullPointer + err = std_json.Unmarshal([]byte(`{"opt_null":"2006-01-02T15:04:05Z"}`), &v3) + assert.NoError(t, err) + assert.True(t, v3.OptNull.Has()) + assert.NotNil(t, v3.OptNull.Value()) + }) } func TestOptionalFromJson(t *testing.T) { diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 07c82a7b4bd..90051f5307e 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -10,6 +10,8 @@ import ( "strings" "time" + "gitea.dev/modules/optional" + "go.yaml.in/yaml/v4" ) @@ -96,7 +98,7 @@ type CreateIssueOption struct { Assignee string `json:"assignee"` Assignees []string `json:"assignees"` // swagger:strfmt date-time - Deadline *time.Time `json:"due_date"` + Deadline optional.Option[*string] `json:"due_date,omitzero"` // milestone id Milestone int64 `json:"milestone"` // list of label ids @@ -118,9 +120,10 @@ type EditIssueOption struct { // list of project ids to set (replaces existing projects) Projects *[]int64 `json:"projects"` State *string `json:"state"` + // Deadline is the updated due date for the issue, use null or empty string to remove the deadline // swagger:strfmt date-time - Deadline *time.Time `json:"due_date"` - RemoveDeadline *bool `json:"unset_due_date"` + Deadline optional.Option[*string] `json:"due_date,omitzero"` + RemoveDeadline *bool `json:"unset_due_date"` // The current version of the issue content to detect conflicts during editing ContentVersion *int `json:"content_version"` } @@ -130,10 +133,11 @@ type IssueAssigneesOption struct { Assignees []string `json:"assignees"` } -// EditDeadlineOption options for creating a deadline +// EditDeadlineOption options for editing a deadline type EditDeadlineOption struct { - // required:true - Deadline *time.Time `json:"due_date"` + // Deadline is the updated due date for the issue or pull request, use null or empty string to remove the deadline + // swagger:strfmt date-time + Deadline optional.Option[*string] `json:"due_date,omitzero"` } // IssueDeadline represents an issue deadline diff --git a/modules/structs/issue_milestone.go b/modules/structs/issue_milestone.go index dd8bdc6cda7..150a2395aaa 100644 --- a/modules/structs/issue_milestone.go +++ b/modules/structs/issue_milestone.go @@ -5,9 +5,11 @@ package structs import ( "time" + + "gitea.dev/modules/optional" ) -// Milestone milestone is a collection of issues on one repository +// Milestone is a collection of issues on one repository type Milestone struct { // ID is the unique identifier for the milestone ID int64 `json:"id"` @@ -39,7 +41,7 @@ type CreateMilestoneOption struct { Description string `json:"description"` // swagger:strfmt date-time // Deadline is the due date for the milestone - Deadline *time.Time `json:"due_on"` + Deadline optional.Option[*string] `json:"due_on,omitzero"` // enum: ["open","closed"] // State indicates the initial state of the milestone State string `json:"state"` @@ -54,6 +56,7 @@ type EditMilestoneOption struct { // State indicates the updated state of the milestone // enum: ["open","closed"] State *string `json:"state"` - // Deadline is the updated due date for the milestone - Deadline *time.Time `json:"due_on"` + // Deadline is the due date for the milestone, use null or empty string to remove the deadline + // swagger:strfmt date-time + Deadline optional.Option[*string] `json:"due_on,omitzero"` } diff --git a/modules/structs/pull.go b/modules/structs/pull.go index cd2ffbe7195..341fd57fc7d 100644 --- a/modules/structs/pull.go +++ b/modules/structs/pull.go @@ -5,6 +5,8 @@ package structs import ( "time" + + "gitea.dev/modules/optional" ) // PullRequest represents a pull request @@ -137,7 +139,7 @@ type CreatePullRequestOption struct { // The list of label IDs to assign to the pull request Labels []int64 `json:"labels"` // swagger:strfmt date-time - Deadline *time.Time `json:"due_date"` + Deadline optional.Option[*string] `json:"due_date,omitzero"` // The list of reviewer usernames Reviewers []string `json:"reviewers"` // The list of team reviewer names @@ -164,8 +166,9 @@ type EditPullRequestOption struct { Labels []int64 `json:"labels"` // The new state for the pull request State *string `json:"state"` + // Deadline is the updated due date for the pull request, use null or empty string to remove the deadline // swagger:strfmt date-time - Deadline *time.Time `json:"due_date"` + Deadline optional.Option[*string] `json:"due_date,omitzero"` // Whether to remove the current deadline RemoveDeadline *bool `json:"unset_due_date"` // Whether to allow maintainer edits diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 938e13bc654..6a20b0ff3c6 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -10,7 +10,6 @@ import ( "net/http" "strconv" "strings" - "time" "gitea.dev/models/db" issues_model "gitea.dev/models/issues" @@ -577,9 +576,12 @@ func CreateIssue(ctx *context.APIContext) { } assigneeIDs := make([]int64, 0) - var err error if ctx.Repo.Permission.CanWrite(unit.TypeIssues) { - issue.DeadlineUnix = common.ParseAPIDeadlineToEndOfDay(form.Deadline) + _, err := common.ApplyAPIDeadlineTimestamp(&issue.DeadlineUnix, form.Deadline, nil) + if err != nil { + ctx.APIErrorAuto(err) + return + } issue.MilestoneID = form.Milestone assigneeIDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees) if err != nil { @@ -637,6 +639,7 @@ func CreateIssue(ctx *context.APIContext) { } // Refetch from database to assign some automatic values + var err error issue, err = issues_model.GetIssueByID(ctx, issue.ID) if err != nil { ctx.APIErrorInternal(err) @@ -890,22 +893,20 @@ func DeleteIssue(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } -func editIssueDeadline(ctx *context.APIContext, issue *issues_model.Issue, deadline *time.Time, removeDeadline *bool) bool { - if deadline == nil && removeDeadline == nil { +func editIssueDeadline(ctx *context.APIContext, issue *issues_model.Issue, deadline optional.Option[*string], removeDeadline *bool) bool { + newDeadlineUnix := issue.DeadlineUnix + changed, err := common.ApplyAPIDeadlineTimestamp(&newDeadlineUnix, deadline, removeDeadline) + if err != nil { + ctx.APIErrorAuto(err) + return false + } + if !changed { return true } - if removeDeadline != nil && *removeDeadline { - deadline = nil - } else if deadline == nil { - ctx.APIError(http.StatusBadRequest, "The due_date cannot be empty") + if err := issues_model.UpdateIssueDeadline(ctx, issue, newDeadlineUnix, ctx.Doer); err != nil { + ctx.APIErrorAuto(err) return false } - deadlineUnix := common.ParseAPIDeadlineToEndOfDay(deadline) - if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil { - ctx.APIErrorInternal(err) - return false - } - issue.DeadlineUnix = deadlineUnix return true } @@ -962,13 +963,20 @@ func UpdateIssueDeadline(ctx *context.APIContext) { return } - deadlineUnix := common.ParseAPIDeadlineToEndOfDay(form.Deadline) - if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil { - ctx.APIErrorInternal(err) + newDeadlineUnix := issue.DeadlineUnix + changed, err := common.ApplyAPIDeadlineTimestamp(&newDeadlineUnix, form.Deadline, nil) + if err != nil { + ctx.APIErrorAuto(err) return } - - ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: deadlineUnix.AsTimePtr()}) + if changed { + if err := issues_model.UpdateIssueDeadline(ctx, issue, newDeadlineUnix, ctx.Doer); err != nil { + ctx.APIErrorAuto(err) + return + } + } + // StatusCreated is the copied & pasted garbage + ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: issue.DeadlineUnix.AsTimePtr()}) } func closeOrReopenIssue(ctx *context.APIContext, issue *issues_model.Issue, state api.StateType) { diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go index 41a7361e3ca..66531ac3583 100644 --- a/routers/api/v1/repo/milestone.go +++ b/routers/api/v1/repo/milestone.go @@ -150,12 +150,15 @@ func CreateMilestone(ctx *context.APIContext) { form := web.GetForm[*api.CreateMilestoneOption](ctx) milestone := &issues_model.Milestone{ - RepoID: ctx.Repo.Repository.ID, - Name: form.Title, - Content: form.Description, - DeadlineUnix: common.ParseAPIDeadlineToEndOfDay(form.Deadline), + RepoID: ctx.Repo.Repository.ID, + Name: form.Title, + Content: form.Description, + } + _, err := common.ApplyAPIDeadlineTimestamp(&milestone.DeadlineUnix, form.Deadline, nil) + if err != nil { + ctx.APIErrorAuto(err) + return } - if form.State == "closed" { milestone.IsClosed = true milestone.ClosedDateUnix = timeutil.TimeStampNow() @@ -204,7 +207,7 @@ func EditMilestone(ctx *context.APIContext) { // "$ref": "#/responses/notFound" form := web.GetForm[*api.EditMilestoneOption](ctx) milestone := getMilestoneByIDOrName(ctx) - if ctx.Written() { + if milestone == nil { return } @@ -214,7 +217,10 @@ func EditMilestone(ctx *context.APIContext) { if form.Description != nil { milestone.Content = *form.Description } - milestone.DeadlineUnix = common.ParseAPIDeadlineToEndOfDay(form.Deadline) + if _, err := common.ApplyAPIDeadlineTimestamp(&milestone.DeadlineUnix, form.Deadline, nil); err != nil { + ctx.APIErrorAuto(err) + return + } oldIsClosed := milestone.IsClosed if form.State != nil { diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 6a391f656c9..6719d758631 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -496,15 +496,20 @@ func CreatePullRequest(ctx *context.APIContext) { } prIssue := &issues_model.Issue{ - RepoID: repo.ID, - Title: form.Title, - PosterID: ctx.Doer.ID, - Poster: ctx.Doer, - MilestoneID: milestoneID, - IsPull: true, - Content: form.Body, - DeadlineUnix: common.ParseAPIDeadlineToEndOfDay(form.Deadline), + RepoID: repo.ID, + Title: form.Title, + PosterID: ctx.Doer.ID, + Poster: ctx.Doer, + MilestoneID: milestoneID, + IsPull: true, + Content: form.Body, } + _, err = common.ApplyAPIDeadlineTimestamp(&prIssue.DeadlineUnix, form.Deadline, nil) + if err != nil { + ctx.APIErrorAuto(err) + return + } + pr := &issues_model.PullRequest{ HeadRepoID: compareResult.HeadRepo.ID, BaseRepoID: repo.ID, diff --git a/routers/common/deadline.go b/routers/common/deadline.go index 43b30462835..f98c6e0d300 100644 --- a/routers/common/deadline.go +++ b/routers/common/deadline.go @@ -6,8 +6,10 @@ package common import ( "time" + "gitea.dev/modules/optional" "gitea.dev/modules/setting" "gitea.dev/modules/timeutil" + "gitea.dev/modules/util" ) func ParseDeadlineDateToEndOfDay(date string) (timeutil.TimeStamp, error) { @@ -22,9 +24,33 @@ func ParseDeadlineDateToEndOfDay(date string) (timeutil.TimeStamp, error) { return timeutil.TimeStamp(deadline.Unix()), nil } -func ParseAPIDeadlineToEndOfDay(t *time.Time) timeutil.TimeStamp { - if t == nil || t.IsZero() || t.Unix() == 0 { - return 0 +func ApplyAPIDeadlineTimestamp(field *timeutil.TimeStamp, input optional.Option[*string], remove *bool) (changed bool, err error) { + old := *field + if remove != nil && *remove || input.Has() && (input.Value() == nil || *input.Value() == "") { + *field = 0 + return old != *field, nil } - return timeutil.TimeStamp(time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, setting.DefaultUILocation).Unix()) + + if !input.Has() { + return false, nil + } + inputValue := *input.Value() + + formats := []string{ + "2006-01-02", + "2006-01-02 15:04:05", + time.RFC3339, + } + for _, format := range formats { + t, err := time.ParseInLocation(format, inputValue, setting.DefaultUILocation) + if err == nil { + ts := time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, setting.DefaultUILocation).Unix() + if ts < 86400*2 { + ts = 0 // tolerate timezone difference between server and client + } + *field = timeutil.TimeStamp(ts) + return old != *field, nil + } + } + return false, util.NewInvalidArgumentErrorf("invalid date format: %s", inputValue) } diff --git a/templates/swagger/v1-openapi3.generated.json b/templates/swagger/v1-openapi3.generated.json index 156b585bc58..32f048ea82c 100644 --- a/templates/swagger/v1-openapi3.generated.json +++ b/templates/swagger/v1-openapi3.generated.json @@ -5562,17 +5562,15 @@ "x-go-package": "gitea.dev/modules/structs" }, "EditDeadlineOption": { - "description": "EditDeadlineOption options for creating a deadline", + "description": "EditDeadlineOption options for editing a deadline", "properties": { "due_date": { + "description": "Deadline is the updated due date for the issue or pull request, use null or empty string to remove the deadline", "format": "date-time", "type": "string", "x-go-name": "Deadline" } }, - "required": [ - "due_date" - ], "type": "object", "x-go-package": "gitea.dev/modules/structs" }, @@ -5673,6 +5671,7 @@ "x-go-name": "ContentVersion" }, "due_date": { + "description": "Deadline is the updated due date for the issue, use null or empty string to remove the deadline", "format": "date-time", "type": "string", "x-go-name": "Deadline" @@ -5752,7 +5751,7 @@ "x-go-name": "Description" }, "due_on": { - "description": "Deadline is the updated due date for the milestone", + "description": "Deadline is the due date for the milestone, use null or empty string to remove the deadline", "format": "date-time", "type": "string", "x-go-name": "Deadline" @@ -5902,6 +5901,7 @@ "x-go-name": "ContentVersion" }, "due_date": { + "description": "Deadline is the updated due date for the pull request, use null or empty string to remove the deadline", "format": "date-time", "type": "string", "x-go-name": "Deadline" @@ -7948,7 +7948,7 @@ "x-go-package": "gitea.dev/modules/structs" }, "Milestone": { - "description": "Milestone milestone is a collection of issues on one repository", + "description": "Milestone is a collection of issues on one repository", "properties": { "closed_at": { "format": "date-time", diff --git a/templates/swagger/v1-swagger.generated.json b/templates/swagger/v1-swagger.generated.json index ec9a3144ede..2295cb1d605 100644 --- a/templates/swagger/v1-swagger.generated.json +++ b/templates/swagger/v1-swagger.generated.json @@ -28658,13 +28658,11 @@ "x-go-package": "gitea.dev/modules/structs" }, "EditDeadlineOption": { - "description": "EditDeadlineOption options for creating a deadline", + "description": "EditDeadlineOption options for editing a deadline", "type": "object", - "required": [ - "due_date" - ], "properties": { "due_date": { + "description": "Deadline is the updated due date for the issue or pull request, use null or empty string to remove the deadline", "type": "string", "format": "date-time", "x-go-name": "Deadline" @@ -28769,6 +28767,7 @@ "x-go-name": "ContentVersion" }, "due_date": { + "description": "Deadline is the updated due date for the issue, use null or empty string to remove the deadline", "type": "string", "format": "date-time", "x-go-name": "Deadline" @@ -28848,7 +28847,7 @@ "x-go-name": "Description" }, "due_on": { - "description": "Deadline is the updated due date for the milestone", + "description": "Deadline is the due date for the milestone, use null or empty string to remove the deadline", "type": "string", "format": "date-time", "x-go-name": "Deadline" @@ -29008,6 +29007,7 @@ "x-go-name": "ContentVersion" }, "due_date": { + "description": "Deadline is the updated due date for the pull request, use null or empty string to remove the deadline", "type": "string", "format": "date-time", "x-go-name": "Deadline" @@ -31055,7 +31055,7 @@ "x-go-package": "gitea.dev/modules/structs" }, "Milestone": { - "description": "Milestone milestone is a collection of issues on one repository", + "description": "Milestone is a collection of issues on one repository", "type": "object", "properties": { "closed_at": { diff --git a/tests/integration/api_issue_milestone_test.go b/tests/integration/api_issue_milestone_test.go index b5526bf0d04..cb3f87072fd 100644 --- a/tests/integration/api_issue_milestone_test.go +++ b/tests/integration/api_issue_milestone_test.go @@ -41,6 +41,7 @@ func TestAPIIssuesMilestone(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) apiMilestone := DecodeJSON(t, resp, &structs.Milestone{}) assert.EqualValues(t, "closed", apiMilestone.State) + assert.NotNil(t, apiMilestone.Deadline, "an omitted due_on keeps the due date") req = NewRequest(t, "GET", urlStr). AddTokenAuth(token) @@ -48,6 +49,10 @@ func TestAPIIssuesMilestone(t *testing.T) { apiMilestone2 := DecodeJSON(t, resp, &structs.Milestone{}) assert.EqualValues(t, "closed", apiMilestone2.State) + req = NewRequestWithJSON(t, "PATCH", urlStr, map[string]any{"due_on": ""}).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + assert.Nil(t, DecodeJSON(t, resp, &structs.Milestone{}).Deadline) + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/milestones", owner.Name, repo.Name), structs.CreateMilestoneOption{ Title: "wow", Description: "closed one", diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go index d53088ffeb4..ed361a93305 100644 --- a/tests/integration/api_issue_test.go +++ b/tests/integration/api_issue_test.go @@ -18,6 +18,7 @@ import ( repo_model "gitea.dev/models/repo" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" + "gitea.dev/modules/optional" "gitea.dev/modules/setting" api "gitea.dev/modules/structs" "gitea.dev/modules/test" @@ -126,7 +127,7 @@ func testAPICreateIssue(t *testing.T) { Body: body, Title: title, Assignee: owner.Name, - Deadline: new(time.Date(2026, 5, 11, 12, 0, 0, 0, time.FixedZone("", -10*3600))), + Deadline: optional.Some(new("2026-05-11T12:00:00-10:00")), }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusCreated) apiIssue := DecodeJSON(t, resp, &api.Issue{}) diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go index b4ebc31f5ec..b2464d10a20 100644 --- a/tests/integration/api_pull_test.go +++ b/tests/integration/api_pull_test.go @@ -20,6 +20,7 @@ import ( unit_model "gitea.dev/models/unit" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" + "gitea.dev/modules/optional" "gitea.dev/modules/setting" api "gitea.dev/modules/structs" "gitea.dev/modules/util" @@ -491,7 +492,7 @@ func TestAPIEditPull(t *testing.T) { Head: "develop", Base: "master", Title: title, - Deadline: new(time.Date(2026, 5, 11, 12, 0, 0, 0, time.FixedZone("", -10*3600))), + Deadline: optional.Some(new("2026-05-11T12:00:00-10:00")), }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusCreated) apiPull := DecodeJSON(t, resp, &api.PullRequest{}) @@ -521,10 +522,20 @@ func TestAPIEditPull(t *testing.T) { }).AddTokenAuth(token) MakeRequest(t, req, http.StatusNotFound) - req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), &api.EditPullRequestOption{ - RemoveDeadline: new(false), - }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusBadRequest) + req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), map[string]any{"due_date": "2000-01-02"}).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: pull.IssueID}) + assert.NotZero(t, issue.DeadlineUnix) + + req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), map[string]any{}).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: pull.IssueID}) + assert.NotZero(t, issue.DeadlineUnix) + + req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), map[string]any{"due_date": nil}).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: pull.IssueID}) + assert.Zero(t, issue.DeadlineUnix) t.Run("PullContentVersion", func(t *testing.T) { testAPIPullContentVersion(t, pull.ID)