mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-09 17:37:20 -06:00
The board reported every card of the target column on each drag, and the server wrote all of them into that column. A stale board in another browser therefore pulled cards moved by someone else back to their old column. The client now sends only the dragged issue and its predecessor, and the server computes the position in one transaction: - Only the dragged issue changes column, the others are just renumbered. - A predecessor that is no longer in the column puts the card at the end. Added an e2e test. Fixes https://github.com/go-gitea/gitea/issues/21243 Fixes https://github.com/go-gitea/gitea/issues/32070 Fixes https://github.com/go-gitea/gitea/issues/13498 --- Assisted-by: Codet:claude --------- Co-authored-by: silverwind <me@silverwind.io>
93 lines
3.4 KiB
Go
93 lines
3.4 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package project
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
issues_model "gitea.dev/models/issues"
|
|
project_model "gitea.dev/models/project"
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/models/unittest"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/services/contexttest"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
unittest.MainTest(m)
|
|
}
|
|
|
|
// TestFindColumn covers the scoping rule every board handler depends on: a request only
|
|
// resolves projects owned by the scope its route assigned, so an ID belonging to anyone
|
|
// else reads as not found rather than leaking across owners.
|
|
func TestFindColumn(t *testing.T) {
|
|
unittest.PrepareTestEnv(t)
|
|
for _, tc := range []struct {
|
|
name string
|
|
projectID string
|
|
columnID string
|
|
doerID int64
|
|
repoScoped bool
|
|
resolves bool
|
|
}{
|
|
{"repository project", "1", "2", 2, true, true},
|
|
{"owner project", "4", "4", 2, false, true},
|
|
{"repository board cannot reach an owner project", "4", "1", 2, true, false},
|
|
{"owner board cannot reach another owner's project", "4", "4", 1, false, false},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx, _ := contexttest.MockContext(t, "user2/-/projects")
|
|
contexttest.LoadUser(t, ctx, tc.doerID)
|
|
if tc.repoScoped {
|
|
contexttest.LoadRepo(t, ctx, 1)
|
|
} else {
|
|
ctx.ContextUser = ctx.Doer
|
|
}
|
|
ctx.SetPathParam("id", tc.projectID)
|
|
ctx.SetPathParam("columnID", tc.columnID)
|
|
|
|
project, column := findColumn(ctx)
|
|
assert.Equal(t, tc.resolves, project != nil)
|
|
assert.Equal(t, tc.resolves, column != nil)
|
|
if tc.resolves {
|
|
assert.False(t, ctx.Written())
|
|
} else {
|
|
// a foreign ID must read as not found, never as a 500
|
|
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMoveIssuesNeedsOnlyProjectWrite(t *testing.T) {
|
|
unittest.PrepareTestEnv(t)
|
|
require.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(t.Context(), &repo_model.Repository{ID: 3, IsArchived: true}, "is_archived"))
|
|
ctx, _ := contexttest.MockContext(t, "POST /org3/-/projects")
|
|
contexttest.LoadUser(t, ctx, 15)
|
|
ctx.ContextUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
|
project := &project_model.Project{Title: "board", OwnerID: 3, CreatorID: 2, Type: project_model.TypeOrganization}
|
|
require.NoError(t, project_model.NewProject(ctx, project))
|
|
require.NoError(t, project_model.NewColumn(ctx, &project_model.Column{Title: "Backlog", ProjectID: project.ID, Default: true}))
|
|
column := &project_model.Column{Title: "Done", ProjectID: project.ID}
|
|
require.NoError(t, project_model.NewColumn(ctx, column))
|
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6})
|
|
_, _, err := issues_model.IssueAssignOrRemoveProject(ctx, issue, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}), []int64{project.ID})
|
|
require.NoError(t, err)
|
|
ctx.SetPathParam("id", strconv.FormatInt(project.ID, 10))
|
|
ctx.SetPathParam("columnID", strconv.FormatInt(column.ID, 10))
|
|
ctx.Req.Body = io.NopCloser(strings.NewReader(fmt.Sprintf(`{"issueID":%d,"prevIssueID":0}`, issue.ID)))
|
|
|
|
MoveIssues(ctx)
|
|
assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
|
|
unittest.AssertExistsAndLoadBean(t, &project_model.ProjectIssue{IssueID: issue.ID, ProjectID: project.ID, ProjectColumnID: column.ID})
|
|
}
|