Merge pull request 'Add an 'updated_at' field to the EditIssueOption struct' (#764) from fluzz/forgejo:add_update_at into forgejo-development

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/764
This commit is contained in:
Earl Warren 2023-08-26 13:33:30 +00:00
commit 4af241b9fb
26 changed files with 795 additions and 32 deletions

View file

@ -823,6 +823,11 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment,
IsForcePush: opts.IsForcePush,
Invalidated: opts.Invalidated,
}
if opts.Issue.NoAutoTime {
comment.CreatedUnix = opts.Issue.UpdatedUnix
comment.UpdatedUnix = opts.Issue.UpdatedUnix
e.NoAutoTime()
}
if _, err = e.Insert(comment); err != nil {
return nil, err
}
@ -1101,9 +1106,17 @@ func UpdateComment(c *Comment, doer *user_model.User) error {
return err
}
defer committer.Close()
sess := db.GetEngine(ctx)
if _, err := sess.ID(c.ID).AllCols().Update(c); err != nil {
sess := db.GetEngine(ctx).ID(c.ID).AllCols()
if c.Issue.NoAutoTime {
// update the DataBase
sess = sess.NoAutoTime().SetExpr("updated_unix", c.Issue.UpdatedUnix)
// the UpdatedUnix value of the Comment also has to be set,
// to return the adequate valuè
// see https://codeberg.org/forgejo/forgejo/pulls/764#issuecomment-1023801
c.UpdatedUnix = c.Issue.UpdatedUnix
}
if _, err := sess.Update(c); err != nil {
return err
}
if err := c.LoadIssue(ctx); err != nil {

View file

@ -125,6 +125,7 @@ type Issue struct {
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
ClosedUnix timeutil.TimeStamp `xorm:"INDEX"`
NoAutoTime bool `xorm:"-"`
Attachments []*repo_model.Attachment `xorm:"-"`
Comments CommentList `xorm:"-"`

View file

@ -27,7 +27,12 @@ import (
// UpdateIssueCols updates cols of issue
func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error {
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil {
sess := db.GetEngine(ctx).ID(issue.ID)
if issue.NoAutoTime {
cols = append(cols, []string{"updated_unix"}...)
sess.NoAutoTime()
}
if _, err := sess.Cols(cols...).Update(issue); err != nil {
return err
}
return nil
@ -71,7 +76,11 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use
}
if issue.IsClosed {
issue.ClosedUnix = timeutil.TimeStampNow()
if issue.NoAutoTime {
issue.ClosedUnix = issue.UpdatedUnix
} else {
issue.ClosedUnix = timeutil.TimeStampNow()
}
} else {
issue.ClosedUnix = 0
}
@ -92,8 +101,14 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use
// Update issue count of milestone
if issue.MilestoneID > 0 {
if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
return nil, err
if issue.NoAutoTime {
if err := UpdateMilestoneCountersWithDate(ctx, issue.MilestoneID, issue.UpdatedUnix); err != nil {
return nil, err
}
} else {
if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
return nil, err
}
}
}
@ -259,8 +274,12 @@ func ChangeIssueContent(issue *Issue, doer *user_model.User, content string) (er
return fmt.Errorf("UpdateIssueCols: %w", err)
}
historyDate := timeutil.TimeStampNow()
if issue.NoAutoTime {
historyDate = issue.UpdatedUnix
}
if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0,
timeutil.TimeStampNow(), issue.Content, false); err != nil {
historyDate, issue.Content, false); err != nil {
return fmt.Errorf("SaveIssueContentHistory: %w", err)
}
@ -449,10 +468,13 @@ func UpdateIssueByAPI(issue *Issue, doer *user_model.User) (statusChangeComment
return nil, false, err
}
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(
"name", "content", "milestone_id", "priority",
"deadline_unix", "updated_unix", "is_locked").
Update(issue); err != nil {
sess := db.GetEngine(ctx).ID(issue.ID)
cols := []string{"name", "content", "milestone_id", "priority", "deadline_unix", "is_locked"}
if issue.NoAutoTime {
cols = append(cols, "updated_unix")
sess.NoAutoTime()
}
if _, err := sess.Cols(cols...).Update(issue); err != nil {
return nil, false, err
}
@ -498,7 +520,7 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *us
defer committer.Close()
// Update the deadline
if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil {
if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix, NoAutoTime: issue.NoAutoTime, UpdatedUnix: issue.UpdatedUnix}, "deadline_unix"); err != nil {
return err
}

View file

@ -110,6 +110,10 @@ func (issue *Issue) createCrossReferences(stdCtx context.Context, ctx *crossRefe
if ctx.OrigComment != nil {
refCommentID = ctx.OrigComment.ID
}
if ctx.OrigIssue.NoAutoTime {
xref.Issue.NoAutoTime = true
xref.Issue.UpdatedUnix = ctx.OrigIssue.UpdatedUnix
}
opts := &CreateCommentOptions{
Type: ctx.Type,
Doer: ctx.Doer,

View file

@ -188,10 +188,9 @@ func updateMilestone(ctx context.Context, m *Milestone) error {
return UpdateMilestoneCounters(ctx, m.ID)
}
// UpdateMilestoneCounters calculates NumIssues, NumClosesIssues and Completeness
func UpdateMilestoneCounters(ctx context.Context, id int64) error {
func updateMilestoneCounters(ctx context.Context, id int64, noAutoTime bool, updatedUnix timeutil.TimeStamp) error {
e := db.GetEngine(ctx)
_, err := e.ID(id).
sess := e.ID(id).
SetExpr("num_issues", builder.Select("count(*)").From("issue").Where(
builder.Eq{"milestone_id": id},
)).
@ -200,8 +199,11 @@ func UpdateMilestoneCounters(ctx context.Context, id int64) error {
"milestone_id": id,
"is_closed": true,
},
)).
Update(&Milestone{})
))
if noAutoTime {
sess.SetExpr("updated_unix", updatedUnix).NoAutoTime()
}
_, err := sess.Update(&Milestone{})
if err != nil {
return err
}
@ -211,6 +213,16 @@ func UpdateMilestoneCounters(ctx context.Context, id int64) error {
return err
}
// UpdateMilestoneCounters calculates NumIssues, NumClosesIssues and Completeness
func UpdateMilestoneCounters(ctx context.Context, id int64) error {
return updateMilestoneCounters(ctx, id, false, 0)
}
// UpdateMilestoneCountersWithDate calculates NumIssues, NumClosesIssues and Completeness and set the UpdatedUnix date
func UpdateMilestoneCountersWithDate(ctx context.Context, id int64, updatedUnix timeutil.TimeStamp) error {
return updateMilestoneCounters(ctx, id, true, updatedUnix)
}
// ChangeMilestoneStatusByRepoIDAndID changes a milestone open/closed status if the milestone ID is in the repo.
func ChangeMilestoneStatusByRepoIDAndID(repoID, milestoneID int64, isClosed bool) error {
ctx, committer, err := db.TxContext(db.DefaultContext)

View file

@ -28,6 +28,7 @@ type Attachment struct {
Name string
DownloadCount int64 `xorm:"DEFAULT 0"`
Size int64 `xorm:"DEFAULT 0"`
NoAutoTime bool `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
CustomDownloadURL string `xorm:"-"`
}

View file

@ -110,6 +110,8 @@ type EditIssueOption struct {
// swagger:strfmt date-time
Deadline *time.Time `json:"due_date"`
RemoveDeadline *bool `json:"unset_due_date"`
// swagger:strfmt date-time
Updated *time.Time `json:"updated_at"`
}
// EditDeadlineOption options for creating a deadline

View file

@ -28,12 +28,16 @@ type Comment struct {
type CreateIssueCommentOption struct {
// required:true
Body string `json:"body" binding:"Required"`
// swagger:strfmt date-time
Updated *time.Time `json:"updated_at"`
}
// EditIssueCommentOption options for editing a comment
type EditIssueCommentOption struct {
// required: true
Body string `json:"body" binding:"Required"`
// swagger:strfmt date-time
Updated *time.Time `json:"updated_at"`
}
// TimelineComment represents a timeline comment (comment of any type) on a commit or issue

View file

@ -4,6 +4,10 @@
package structs
import (
"time"
)
// Label a label to an issue or a pr
// swagger:model
type Label struct {
@ -45,10 +49,18 @@ type EditLabelOption struct {
IsArchived *bool `json:"is_archived"`
}
// DeleteLabelOption options for deleting a label
type DeleteLabelsOption struct {
// swagger:strfmt date-time
Updated *time.Time `json:"updated_at"`
}
// IssueLabelsOption a collection of labels
type IssueLabelsOption struct {
// list of label IDs
Labels []int64 `json:"labels"`
// swagger:strfmt date-time
Updated *time.Time `json:"updated_at"`
}
// LabelTemplate info of a Label template

View file

@ -1201,8 +1201,8 @@ func Routes() *web.Route {
m.Combo("").Get(repo.ListIssueLabels).
Post(reqToken(), bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
Put(reqToken(), bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
Delete(reqToken(), repo.ClearIssueLabels)
m.Delete("/{id}", reqToken(), repo.DeleteIssueLabel)
Delete(reqToken(), bind(api.DeleteLabelsOption{}), repo.ClearIssueLabels)
m.Delete("/{id}", reqToken(), bind(api.DeleteLabelsOption{}), repo.DeleteIssueLabel)
})
m.Group("/times", func() {
m.Combo("").

View file

@ -774,6 +774,12 @@ func EditIssue(ctx *context.APIContext) {
return
}
err = issue_service.SetIssueUpdateDate(ctx, issue, form.Updated, ctx.Doer)
if err != nil {
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
return
}
oldTitle := issue.Title
if len(form.Title) > 0 {
issue.Title = form.Title

View file

@ -5,6 +5,7 @@ package repo
import (
"net/http"
"time"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
@ -141,6 +142,11 @@ func CreateIssueAttachment(ctx *context.APIContext) {
// description: name of the attachment
// type: string
// required: false
// - name: updated_at
// in: query
// description: time of the attachment's creation. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: attachment
// in: formData
// description: attachment to upload
@ -163,6 +169,20 @@ func CreateIssueAttachment(ctx *context.APIContext) {
return
}
updatedAt := ctx.Req.FormValue("updated_at")
if len(updatedAt) != 0 {
updated, err := time.Parse(time.RFC3339, updatedAt)
if err != nil {
ctx.Error(http.StatusInternalServerError, "time.Parse", err)
return
}
err = issue_service.SetIssueUpdateDate(ctx, issue, &updated, ctx.Doer)
if err != nil {
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
return
}
}
// Get uploaded file from request
file, header, err := ctx.Req.FormFile("attachment")
if err != nil {
@ -177,10 +197,12 @@ func CreateIssueAttachment(ctx *context.APIContext) {
}
attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
IssueID: issue.ID,
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
IssueID: issue.ID,
NoAutoTime: issue.NoAutoTime,
CreatedUnix: issue.UpdatedUnix,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)

View file

@ -362,6 +362,12 @@ func CreateIssueComment(ctx *context.APIContext) {
return
}
err = issue_service.SetIssueUpdateDate(ctx, issue, form.Updated, ctx.Doer)
if err != nil {
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
return
}
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
if err != nil {
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
@ -554,6 +560,17 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
return
}
err = comment.LoadIssue(ctx)
if err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
}
err = issue_service.SetIssueUpdateDate(ctx, comment.Issue, form.Updated, ctx.Doer)
if err != nil {
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
return
}
oldContent := comment.Content
comment.Content = form.Body
if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {

View file

@ -5,6 +5,7 @@ package repo
import (
"net/http"
"time"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
@ -144,6 +145,11 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
// description: name of the attachment
// type: string
// required: false
// - name: updated_at
// in: query
// description: time of the attachment's creation. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: attachment
// in: formData
// description: attachment to upload
@ -167,6 +173,25 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
return
}
updatedAt := ctx.Req.FormValue("updated_at")
if len(updatedAt) != 0 {
updated, err := time.Parse(time.RFC3339, updatedAt)
if err != nil {
ctx.Error(http.StatusInternalServerError, "time.Parse", err)
return
}
err = comment.LoadIssue(ctx)
if err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
}
err = issue_service.SetIssueUpdateDate(ctx, comment.Issue, &updated, ctx.Doer)
if err != nil {
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
return
}
}
// Get uploaded file from request
file, header, err := ctx.Req.FormFile("attachment")
if err != nil {
@ -181,11 +206,13 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
}
attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
IssueID: comment.IssueID,
CommentID: comment.ID,
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
IssueID: comment.IssueID,
CommentID: comment.ID,
NoAutoTime: comment.Issue.NoAutoTime,
CreatedUnix: comment.Issue.UpdatedUnix,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)

View file

@ -149,6 +149,10 @@ func DeleteIssueLabel(ctx *context.APIContext) {
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/DeleteLabelsOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
@ -156,6 +160,7 @@ func DeleteIssueLabel(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.DeleteLabelsOption)
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
@ -172,6 +177,11 @@ func DeleteIssueLabel(ctx *context.APIContext) {
return
}
if err := issue_service.SetIssueUpdateDate(ctx, issue, form.Updated, ctx.Doer); err != nil {
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
return
}
label, err := issues_model.GetLabelByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
if issues_model.IsErrLabelNotExist(err) {
@ -269,11 +279,16 @@ func ClearIssueLabels(ctx *context.APIContext) {
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/DeleteLabelsOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
form := web.GetForm(ctx).(*api.DeleteLabelsOption)
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
@ -290,6 +305,11 @@ func ClearIssueLabels(ctx *context.APIContext) {
return
}
if err := issue_service.SetIssueUpdateDate(ctx, issue, form.Updated, ctx.Doer); err != nil {
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
return
}
if err := issue_service.ClearLabels(issue, ctx.Doer); err != nil {
ctx.Error(http.StatusInternalServerError, "ClearLabels", err)
return
@ -320,5 +340,11 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
return nil, nil, nil
}
err = issue_service.SetIssueUpdateDate(ctx, issue, form.Updated, ctx.Doer)
if err != nil {
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
return nil, nil, err
}
return issue, labels, err
}

View file

@ -47,6 +47,9 @@ type swaggerParameterBodies struct {
// in:body
IssueLabelsOption api.IssueLabelsOption
// in:body
DeleteLabelsOption api.DeleteLabelsOption
// in:body
CreateKeyOption api.CreateKeyOption

View file

@ -32,7 +32,12 @@ func NewAttachment(attach *repo_model.Attachment, file io.Reader, size int64) (*
}
attach.Size = size
return db.Insert(ctx, attach)
eng := db.GetEngine(ctx)
if attach.NoAutoTime {
eng.NoAutoTime()
}
_, err = eng.Insert(attach)
return err
})
return attach, err

View file

@ -89,7 +89,11 @@ func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_mode
}
if needsContentHistory {
err := issues_model.SaveIssueContentHistory(ctx, doer.ID, c.IssueID, c.ID, timeutil.TimeStampNow(), c.Content, false)
historyDate := timeutil.TimeStampNow()
if c.Issue.NoAutoTime {
historyDate = c.Issue.UpdatedUnix
}
err := issues_model.SaveIssueContentHistory(ctx, doer.ID, c.IssueID, c.ID, historyDate, c.Content, false)
if err != nil {
return err
}

View file

@ -6,6 +6,7 @@ package issue
import (
"context"
"fmt"
"time"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
@ -18,6 +19,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/timeutil"
)
// NewIssue creates new issue with labels for repository.
@ -304,3 +306,40 @@ func deleteIssue(ctx context.Context, issue *issues_model.Issue) error {
return committer.Commit()
}
// Set the UpdatedUnix date and the NoAutoTime field of an Issue if a non
// nil 'updated' time is provided
//
// In order to set a specific update time, the DB will be updated with
// NoAutoTime(). A 'NoAutoTime' boolean field in the Issue struct is used to
// propagate down to the DB update calls the will to apply autoupdate or not.
func SetIssueUpdateDate(ctx context.Context, issue *issues_model.Issue, updated *time.Time, doer *user_model.User) error {
issue.NoAutoTime = false
if updated == nil {
return nil
}
if err := issue.LoadRepo(ctx); err != nil {
return err
}
// Check if the poster is allowed to set an update date
perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
if err != nil {
return err
}
if !perm.IsAdmin() && !perm.IsOwner() {
return fmt.Errorf("user needs to have admin or owner right")
}
// A simple guard against potential inconsistent calls
updatedUnix := timeutil.TimeStamp(updated.Unix())
if updatedUnix < issue.CreatedUnix || updatedUnix > timeutil.TimeStampNow() {
return fmt.Errorf("unallowed update date")
}
issue.UpdatedUnix = updatedUnix
issue.NoAutoTime = true
return nil
}

View file

@ -13,6 +13,32 @@ import (
"code.gitea.io/gitea/modules/notification"
)
func updateMilestoneCounters(ctx context.Context, issue *issues_model.Issue, id int64) error {
if issue.NoAutoTime {
// We set the milestone's update date to the max of the
// milestone and issue update dates.
// Note: we can not call UpdateMilestoneCounters() if the
// milestone's update date is to be kept, because that function
// auto-updates the dates.
milestone, err := issues_model.GetMilestoneByRepoID(ctx, issue.RepoID, id)
if err != nil {
return fmt.Errorf("GetMilestoneByRepoID: %w", err)
}
updatedUnix := milestone.UpdatedUnix
if issue.UpdatedUnix > updatedUnix {
updatedUnix = issue.UpdatedUnix
}
if err := issues_model.UpdateMilestoneCountersWithDate(ctx, id, updatedUnix); err != nil {
return err
}
} else {
if err := issues_model.UpdateMilestoneCounters(ctx, id); err != nil {
return err
}
}
return nil
}
func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) error {
// Only check if milestone exists if we don't remove it.
if issue.MilestoneID > 0 {
@ -30,13 +56,13 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *is
}
if oldMilestoneID > 0 {
if err := issues_model.UpdateMilestoneCounters(ctx, oldMilestoneID); err != nil {
if err := updateMilestoneCounters(ctx, issue, oldMilestoneID); err != nil {
return err
}
}
if issue.MilestoneID > 0 {
if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
if err := updateMilestoneCounters(ctx, issue, issue.MilestoneID); err != nil {
return err
}
}

View file

@ -6120,6 +6120,13 @@
"name": "name",
"in": "query"
},
{
"type": "string",
"format": "date-time",
"description": "time of the attachment's creation. This is a timestamp in RFC 3339 format",
"name": "updated_at",
"in": "query"
},
{
"type": "file",
"description": "attachment to upload",
@ -6718,6 +6725,13 @@
"name": "name",
"in": "query"
},
{
"type": "string",
"format": "date-time",
"description": "time of the attachment's creation. This is a timestamp in RFC 3339 format",
"name": "updated_at",
"in": "query"
},
{
"type": "file",
"description": "attachment to upload",
@ -7651,6 +7665,13 @@
"name": "index",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/DeleteLabelsOption"
}
}
],
"responses": {
@ -7703,6 +7724,13 @@
"name": "id",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/DeleteLabelsOption"
}
}
],
"responses": {
@ -17026,6 +17054,11 @@
"body": {
"type": "string",
"x-go-name": "Body"
},
"updated_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Updated"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
@ -17784,6 +17817,18 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"DeleteLabelsOption": {
"description": "DeleteLabelOption options for deleting a label",
"type": "object",
"properties": {
"updated_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Updated"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"DeployKey": {
"description": "DeployKey a deploy key",
"type": "object",
@ -18037,6 +18082,11 @@
"body": {
"type": "string",
"x-go-name": "Body"
},
"updated_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Updated"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
@ -18086,6 +18136,11 @@
"unset_due_date": {
"type": "boolean",
"x-go-name": "RemoveDeadline"
},
"updated_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Updated"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
@ -19500,6 +19555,11 @@
"format": "int64"
},
"x-go-name": "Labels"
},
"updated_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Updated"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"

View file

@ -11,6 +11,7 @@ import (
"mime/multipart"
"net/http"
"testing"
"time"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
@ -111,6 +112,82 @@ func TestAPICreateCommentAttachment(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, CommentID: comment.ID})
}
func TestAPICreateCommentAttachmentAutoDate(t *testing.T) {
defer tests.PrepareTestEnv(t)()
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets?token=%s",
repoOwner.Name, repo.Name, comment.ID, token)
filename := "image.png"
buff := generateImg()
body := &bytes.Buffer{}
t.Run("WithAutoDate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Setup multi-part
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("attachment", filename)
assert.NoError(t, err)
_, err = io.Copy(part, &buff)
assert.NoError(t, err)
err = writer.Close()
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", urlStr, body)
req.Header.Add("Content-Type", writer.FormDataContentType())
resp := session.MakeRequest(t, req, http.StatusCreated)
apiAttachment := new(api.Attachment)
DecodeJSON(t, resp, &apiAttachment)
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID})
// the execution of the API call supposedly lasted less than one minute
updatedSince := time.Since(apiAttachment.Created)
assert.LessOrEqual(t, updatedSince, time.Minute)
commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID})
updatedSince = time.Since(commentAfter.UpdatedUnix.AsTime())
assert.LessOrEqual(t, updatedSince, time.Minute)
})
t.Run("WithUpdateDate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
urlStr += fmt.Sprintf("&updated_at=%s", updatedAt.UTC().Format(time.RFC3339))
// Setup multi-part
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("attachment", filename)
assert.NoError(t, err)
_, err = io.Copy(part, &buff)
assert.NoError(t, err)
err = writer.Close()
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", urlStr, body)
req.Header.Add("Content-Type", writer.FormDataContentType())
resp := session.MakeRequest(t, req, http.StatusCreated)
apiAttachment := new(api.Attachment)
DecodeJSON(t, resp, &apiAttachment)
// dates will be converted into the same tz, in order to compare them
utcTZ, _ := time.LoadLocation("UTC")
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID})
assert.Equal(t, updatedAt.In(utcTZ), apiAttachment.Created.In(utcTZ))
commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID})
assert.Equal(t, updatedAt.In(utcTZ), commentAfter.UpdatedUnix.AsTime().In(utcTZ))
})
}
func TestAPIEditCommentAttachment(t *testing.T) {
defer tests.PrepareTestEnv(t)()

View file

@ -8,6 +8,7 @@ import (
"net/http"
"net/url"
"testing"
"time"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
@ -110,6 +111,58 @@ func TestAPICreateComment(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
}
func TestAPICreateCommentAutoDate(t *testing.T) {
defer tests.PrepareTestEnv(t)()
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments?token=%s",
repoOwner.Name, repo.Name, issue.Index, token)
const commentBody = "Comment body"
t.Run("WithAutoDate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
"body": commentBody,
})
resp := MakeRequest(t, req, http.StatusCreated)
var updatedComment api.Comment
DecodeJSON(t, resp, &updatedComment)
// the execution of the API call supposedly lasted less than one minute
updatedSince := time.Since(updatedComment.Updated)
assert.LessOrEqual(t, updatedSince, time.Minute)
commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
updatedSince = time.Since(commentAfter.UpdatedUnix.AsTime())
assert.LessOrEqual(t, updatedSince, time.Minute)
})
t.Run("WithUpdateDate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueCommentOption{
Body: commentBody,
Updated: &updatedAt,
})
resp := MakeRequest(t, req, http.StatusCreated)
var updatedComment api.Comment
DecodeJSON(t, resp, &updatedComment)
// dates will be converted into the same tz, in order to compare them
utcTZ, _ := time.LoadLocation("UTC")
assert.Equal(t, updatedAt.In(utcTZ), updatedComment.Updated.In(utcTZ))
commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
assert.Equal(t, updatedAt.In(utcTZ), commentAfter.UpdatedUnix.AsTime().In(utcTZ))
})
}
func TestAPIGetComment(t *testing.T) {
defer tests.PrepareTestEnv(t)()
@ -161,6 +214,60 @@ func TestAPIEditComment(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID, IssueID: issue.ID, Content: newCommentBody})
}
func TestAPIEditCommentWithDate(t *testing.T) {
defer tests.PrepareTestEnv(t)()
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
unittest.Cond("type = ?", issues_model.CommentTypeComment))
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
repoOwner.Name, repo.Name, comment.ID, token)
const newCommentBody = "This is the new comment body"
t.Run("WithAutoDate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
"body": newCommentBody,
})
resp := MakeRequest(t, req, http.StatusOK)
var updatedComment api.Comment
DecodeJSON(t, resp, &updatedComment)
// the execution of the API call supposedly lasted less than one minute
updatedSince := time.Since(updatedComment.Updated)
assert.LessOrEqual(t, updatedSince, time.Minute)
commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID, IssueID: issue.ID, Content: newCommentBody})
updatedSince = time.Since(commentAfter.UpdatedUnix.AsTime())
assert.LessOrEqual(t, updatedSince, time.Minute)
})
t.Run("WithUpdateDate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditIssueCommentOption{
Body: newCommentBody,
Updated: &updatedAt,
})
resp := MakeRequest(t, req, http.StatusOK)
var updatedComment api.Comment
DecodeJSON(t, resp, &updatedComment)
// dates will be converted into the same tz, in order to compare them
utcTZ, _ := time.LoadLocation("UTC")
assert.Equal(t, updatedAt.In(utcTZ), updatedComment.Updated.In(utcTZ))
commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID, IssueID: issue.ID, Content: newCommentBody})
assert.Equal(t, updatedAt.In(utcTZ), commentAfter.UpdatedUnix.AsTime().In(utcTZ))
})
}
func TestAPIDeleteComment(t *testing.T) {
defer tests.PrepareTestEnv(t)()

View file

@ -11,6 +11,7 @@ import (
"mime/multipart"
"net/http"
"testing"
"time"
auth_model "code.gitea.io/gitea/models/auth"
issues_model "code.gitea.io/gitea/models/issues"
@ -100,6 +101,82 @@ func TestAPICreateIssueAttachment(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
}
func TestAPICreateIssueAttachmentAutoDate(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, repoOwner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets?token=%s",
repoOwner.Name, repo.Name, issue.Index, token)
filename := "image.png"
buff := generateImg()
body := &bytes.Buffer{}
t.Run("WithAutoDate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Setup multi-part
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("attachment", filename)
assert.NoError(t, err)
_, err = io.Copy(part, &buff)
assert.NoError(t, err)
err = writer.Close()
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", urlStr, body)
req.Header.Add("Content-Type", writer.FormDataContentType())
resp := session.MakeRequest(t, req, http.StatusCreated)
apiAttachment := new(api.Attachment)
DecodeJSON(t, resp, &apiAttachment)
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
// the execution of the API call supposedly lasted less than one minute
updatedSince := time.Since(apiAttachment.Created)
assert.LessOrEqual(t, updatedSince, time.Minute)
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.Index})
updatedSince = time.Since(issueAfter.UpdatedUnix.AsTime())
assert.LessOrEqual(t, updatedSince, time.Minute)
})
t.Run("WithUpdateDate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
urlStr += fmt.Sprintf("&updated_at=%s", updatedAt.UTC().Format(time.RFC3339))
// Setup multi-part
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("attachment", filename)
assert.NoError(t, err)
_, err = io.Copy(part, &buff)
assert.NoError(t, err)
err = writer.Close()
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", urlStr, body)
req.Header.Add("Content-Type", writer.FormDataContentType())
resp := session.MakeRequest(t, req, http.StatusCreated)
apiAttachment := new(api.Attachment)
DecodeJSON(t, resp, &apiAttachment)
// dates will be converted into the same tz, in order to compare them
utcTZ, _ := time.LoadLocation("UTC")
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
assert.Equal(t, updatedAt.In(utcTZ), apiAttachment.Created.In(utcTZ))
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID})
assert.Equal(t, updatedAt.In(utcTZ), issueAfter.UpdatedUnix.AsTime().In(utcTZ))
})
}
func TestAPIEditIssueAttachment(t *testing.T) {
defer tests.PrepareTestEnv(t)()

View file

@ -8,6 +8,7 @@ import (
"net/http"
"strings"
"testing"
"time"
auth_model "code.gitea.io/gitea/models/auth"
issues_model "code.gitea.io/gitea/models/issues"
@ -15,6 +16,7 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
@ -111,6 +113,49 @@ func TestAPIAddIssueLabels(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: 2})
}
func TestAPIAddIssueLabelsAutoDate(t *testing.T) {
defer tests.PrepareTestEnv(t)()
issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels?token=%s",
owner.Name, repo.Name, issueBefore.Index, token)
t.Run("WithAutoDate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{
Labels: []int64{1},
})
MakeRequest(t, req, http.StatusOK)
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueBefore.ID})
// the execution of the API call supposedly lasted less than one minute
updatedSince := time.Since(issueAfter.UpdatedUnix.AsTime())
assert.LessOrEqual(t, updatedSince, time.Minute)
})
t.Run("WithUpdatedDate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{
Labels: []int64{2},
Updated: &updatedAt,
})
MakeRequest(t, req, http.StatusOK)
// dates will be converted into the same tz, in order to compare them
utcTZ, _ := time.LoadLocation("UTC")
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueBefore.ID})
assert.Equal(t, updatedAt.In(utcTZ), issueAfter.UpdatedUnix.AsTime().In(utcTZ))
})
}
func TestAPIReplaceIssueLabels(t *testing.T) {
assert.NoError(t, unittest.LoadFixtures())

View file

@ -213,6 +213,157 @@ func TestAPIEditIssue(t *testing.T) {
assert.Equal(t, title, issueAfter.Title)
}
func TestAPIEditIssueAutoDate(t *testing.T) {
defer tests.PrepareTestEnv(t)()
issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 13})
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
assert.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
t.Run("WithAutoDate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// User2 is not owner, but can update the 'public' issue with auto date
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repoBefore.Name, issueBefore.Index, token)
body := "new content!"
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
Body: &body,
})
resp := MakeRequest(t, req, http.StatusCreated)
var apiIssue api.Issue
DecodeJSON(t, resp, &apiIssue)
// the execution of the API call supposedly lasted less than one minute
updatedSince := time.Since(apiIssue.Updated)
assert.LessOrEqual(t, updatedSince, time.Minute)
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueBefore.ID})
updatedSince = time.Since(issueAfter.UpdatedUnix.AsTime())
assert.LessOrEqual(t, updatedSince, time.Minute)
})
t.Run("WithUpdateDate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// User1 is admin, and so can update the issue without auto date
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repoBefore.Name, issueBefore.Index, token)
body := "new content, with updated time"
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
Body: &body,
Updated: &updatedAt,
})
resp := MakeRequest(t, req, http.StatusCreated)
var apiIssue api.Issue
DecodeJSON(t, resp, &apiIssue)
// dates are converted into the same tz, in order to compare them
utcTZ, _ := time.LoadLocation("UTC")
assert.Equal(t, updatedAt.In(utcTZ), apiIssue.Updated.In(utcTZ))
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueBefore.ID})
assert.Equal(t, updatedAt.In(utcTZ), issueAfter.UpdatedUnix.AsTime().In(utcTZ))
})
t.Run("WithoutPermission", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// User2 is not owner nor admin, and so can't update the issue without auto date
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repoBefore.Name, issueBefore.Index, token)
body := "new content, with updated time"
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
Body: &body,
Updated: &updatedAt,
})
resp := MakeRequest(t, req, http.StatusForbidden)
var apiError api.APIError
DecodeJSON(t, resp, &apiError)
assert.Equal(t, "user needs to have admin or owner right", apiError.Message)
})
}
func TestAPIEditIssueMilestoneAutoDate(t *testing.T) {
defer tests.PrepareTestEnv(t)()
issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
assert.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repoBefore.Name, issueBefore.Index, token)
t.Run("WithAutoDate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
milestone := int64(1)
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
Milestone: &milestone,
})
MakeRequest(t, req, http.StatusCreated)
// the execution of the API call supposedly lasted less than one minute
milestoneAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
updatedSince := time.Since(milestoneAfter.UpdatedUnix.AsTime())
assert.LessOrEqual(t, updatedSince, time.Minute)
})
t.Run("WithPostUpdateDate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Note: the updated_unix field of the test Milestones is set to NULL
// Hence, any date is higher than the Milestone's updated date
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
milestone := int64(2)
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
Milestone: &milestone,
Updated: &updatedAt,
})
MakeRequest(t, req, http.StatusCreated)
// the milestone date should be set to 'updatedAt'
// dates are converted into the same tz, in order to compare them
utcTZ, _ := time.LoadLocation("UTC")
milestoneAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
assert.Equal(t, updatedAt.In(utcTZ), milestoneAfter.UpdatedUnix.AsTime().In(utcTZ))
})
t.Run("WithPastUpdateDate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Note: This Milestone's updated_unix has been set to Now() by the first subtest
milestone := int64(1)
milestoneBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
Milestone: &milestone,
Updated: &updatedAt,
})
MakeRequest(t, req, http.StatusCreated)
// the milestone date should not change
// dates are converted into the same tz, in order to compare them
utcTZ, _ := time.LoadLocation("UTC")
milestoneAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
assert.Equal(t, milestoneAfter.UpdatedUnix.AsTime().In(utcTZ), milestoneBefore.UpdatedUnix.AsTime().In(utcTZ))
})
}
func TestAPISearchIssues(t *testing.T) {
defer tests.PrepareTestEnv(t)()