Files
gitea/routers/web/shared/project/project_test.go
T
silverwindandwxiaoguang c62e07f18a fix(projects): only require project write to move board cards (#40161)
Follow-up to https://github.com/go-gitea/gitea/pull/30205, per
https://github.com/go-gitea/gitea/pull/30205#discussion_r4435046605.
Moving a card on a project board is a project operation, so project
write is enough again. Repository boards already show every card to
project writers, who then got a 404 when moving one.

The per-card archived check is removed too. The frontend sends every
card of the target column, so one card from an archived repository
rejected all drops and reorders into that column, silently. Project
workflows already skip archived repositories, and an archived
repository's own board is read-only.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-01 10:46:53 +00:00

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(`{"issues":[{"issueID":%d,"sorting":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})
}