Files
gitea/tests/integration/api_org_project_test.go
Oleks 8ae6245c19 feat(api): add user-scope and org-scope project board endpoints
Adds the missing REST surface that mirrors the existing repo-scope project
API onto user and organization namespaces, so projects at /{owner}/-/projects/
become programmatically manageable (linking issues, listing column membership,
moving cards between columns) without browser-session auth.

Routes registered under /api/v1/users/{username}/projects/... and
/api/v1/orgs/{org}/projects/..., gated by AccessTokenScopeCategoryIssue.
User-scope writes require owner==doer or site admin; org-scope writes
require org membership. Handlers copy the repo-scope shape rather than
refactoring the existing repo handlers, keeping shipping code untouched.

Integration tests cover CRUD, columns, issue add/remove/move, listing per
column, and the permission matrix (owner/non-member/admin) for both scopes.
2026-05-15 15:47:29 +03:00

528 lines
21 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
projects_service "code.gitea.io/gitea/services/projects"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIOrgProjects(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("ListProjects", testAPIOrgListProjects)
t.Run("GetProject", testAPIOrgGetProject)
t.Run("CreateProject", testAPIOrgCreateProject)
t.Run("UpdateProject", testAPIOrgUpdateProject)
t.Run("ChangeProjectStatus", testAPIOrgChangeProjectStatus)
t.Run("DeleteProject", testAPIOrgDeleteProject)
t.Run("ListProjectColumns", testAPIOrgListProjectColumns)
t.Run("CreateProjectColumn", testAPIOrgCreateProjectColumn)
t.Run("UpdateProjectColumn", testAPIOrgUpdateProjectColumn)
t.Run("DeleteProjectColumn", testAPIOrgDeleteProjectColumn)
t.Run("AddIssueToProjectColumn", testAPIOrgAddIssueToProjectColumn)
t.Run("RemoveIssueFromProjectColumn", testAPIOrgRemoveIssueFromProjectColumn)
t.Run("ListProjectColumnIssues", testAPIOrgListProjectColumnIssues)
t.Run("MoveProjectIssue", testAPIOrgMoveProjectIssue)
t.Run("Permissions", testAPIOrgProjectPermissions)
}
// makeOrgProject creates a TypeOrganization project owned by the named org.
// org3 (id=3) is used throughout these tests. Per fixtures/org_user.yml,
// user2 (id=2) and user4 (id=4) are members; user5 is not.
func makeOrgProject(t *testing.T, orgName string, creatorID int64) *project_model.Project {
t.Helper()
org, err := organization.GetOrgByName(t.Context(), orgName)
assert.NoError(t, err)
p := &project_model.Project{
OwnerID: org.ID,
Title: "Test Org Project",
Description: "created by test helper",
CreatorID: creatorID,
Type: project_model.TypeOrganization,
TemplateType: project_model.TemplateTypeNone,
}
assert.NoError(t, project_model.NewProject(t.Context(), p))
return p
}
func testAPIOrgListProjects(t *testing.T) {
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, member.Name, auth_model.AccessTokenScopeReadIssue)
req := NewRequest(t, "GET", "/api/v1/orgs/org3/projects").AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var projects []*api.Project
DecodeJSON(t, resp, &projects)
req = NewRequest(t, "GET", "/api/v1/orgs/org3/projects?state=open").AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &projects)
for _, p := range projects {
assert.Equal(t, api.StateOpen, p.State)
}
req = NewRequest(t, "GET", "/api/v1/orgs/org3/projects?state=all").AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
}
func testAPIOrgGetProject(t *testing.T) {
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
p := makeOrgProject(t, "org3", member.ID)
token := getUserToken(t, member.Name, auth_model.AccessTokenScopeReadIssue)
req := NewRequestf(t, "GET", "/api/v1/orgs/org3/projects/%d", p.ID).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var got api.Project
DecodeJSON(t, resp, &got)
assert.Equal(t, p.ID, got.ID)
assert.Equal(t, p.Title, got.Title)
assert.Equal(t, "organization", got.Type)
req = NewRequest(t, "GET", "/api/v1/orgs/org3/projects/99999").AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func testAPIOrgCreateProject(t *testing.T) {
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, member.Name, auth_model.AccessTokenScopeWriteIssue)
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs/org3/projects", &api.CreateProjectOption{
Title: "Org API Project",
Description: "desc",
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
var got api.Project
DecodeJSON(t, resp, &got)
assert.Equal(t, "Org API Project", got.Title)
assert.Equal(t, "organization", got.Type)
// unauthenticated
req = NewRequestWithJSON(t, "POST", "/api/v1/orgs/org3/projects", &api.CreateProjectOption{Title: "x"})
MakeRequest(t, req, http.StatusUnauthorized)
// empty title
req = NewRequestWithJSON(t, "POST", "/api/v1/orgs/org3/projects", &api.CreateProjectOption{Title: ""}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
// non-member must be forbidden (user5 is not in org3)
nonMember := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user5"})
nmToken := getUserToken(t, nonMember.Name, auth_model.AccessTokenScopeWriteIssue)
req = NewRequestWithJSON(t, "POST", "/api/v1/orgs/org3/projects", &api.CreateProjectOption{Title: "x"}).AddTokenAuth(nmToken)
MakeRequest(t, req, http.StatusForbidden)
}
func testAPIOrgUpdateProject(t *testing.T) {
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
p := makeOrgProject(t, "org3", member.ID)
token := getUserToken(t, member.Name, auth_model.AccessTokenScopeWriteIssue)
newTitle := "Updated Org Project"
newDesc := "Updated desc"
req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/orgs/org3/projects/%d", p.ID), &api.EditProjectOption{
Title: &newTitle,
Description: &newDesc,
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var got api.Project
DecodeJSON(t, resp, &got)
assert.Equal(t, newTitle, got.Title)
assert.Equal(t, newDesc, got.Description)
// non-existent project
req = NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3/projects/99999", &api.EditProjectOption{
Title: &newTitle,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func testAPIOrgChangeProjectStatus(t *testing.T) {
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
p := makeOrgProject(t, "org3", member.ID)
token := getUserToken(t, member.Name, auth_model.AccessTokenScopeWriteIssue)
closed := api.StateClosed
req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/orgs/org3/projects/%d", p.ID), &api.EditProjectOption{
State: &closed,
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var got api.Project
DecodeJSON(t, resp, &got)
assert.Equal(t, api.StateClosed, got.State)
assert.NotNil(t, got.ClosedAt)
open := api.StateOpen
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/orgs/org3/projects/%d", p.ID), &api.EditProjectOption{
State: &open,
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &got)
assert.Equal(t, api.StateOpen, got.State)
bogus := api.StateType("reopen")
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/orgs/org3/projects/%d", p.ID), &api.EditProjectOption{
State: &bogus,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
}
func testAPIOrgDeleteProject(t *testing.T) {
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
p := makeOrgProject(t, "org3", member.ID)
token := getUserToken(t, member.Name, auth_model.AccessTokenScopeWriteIssue)
req := NewRequestf(t, "DELETE", "/api/v1/orgs/org3/projects/%d", p.ID).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequestf(t, "DELETE", "/api/v1/orgs/org3/projects/%d", p.ID).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func testAPIOrgListProjectColumns(t *testing.T) {
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
p := makeOrgProject(t, "org3", member.ID)
token := getUserToken(t, member.Name, auth_model.AccessTokenScopeReadIssue)
for i := 1; i <= 3; i++ {
col := &project_model.Column{
Title: fmt.Sprintf("OrgCol%d", i),
ProjectID: p.ID,
CreatorID: member.ID,
}
assert.NoError(t, project_model.NewColumn(t.Context(), col))
}
req := NewRequestf(t, "GET", "/api/v1/orgs/org3/projects/%d/columns", p.ID).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var cols []*api.ProjectColumn
DecodeJSON(t, resp, &cols)
assert.Len(t, cols, 3)
assert.Equal(t, "3", resp.Header().Get("X-Total-Count"))
req = NewRequestf(t, "GET", "/api/v1/orgs/org3/projects/%d/columns?page=1&limit=2", p.ID).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &cols)
assert.Len(t, cols, 2)
assert.Equal(t, "3", resp.Header().Get("X-Total-Count"))
req = NewRequestf(t, "GET", "/api/v1/orgs/org3/projects/%d/columns?page=2&limit=2", p.ID).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &cols)
assert.Len(t, cols, 1)
assert.Equal(t, "3", resp.Header().Get("X-Total-Count"))
// non-existent project
req = NewRequestf(t, "GET", "/api/v1/orgs/org3/projects/99999/columns").AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func testAPIOrgCreateProjectColumn(t *testing.T) {
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
p := makeOrgProject(t, "org3", member.ID)
token := getUserToken(t, member.Name, auth_model.AccessTokenScopeWriteIssue)
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/org3/projects/%d/columns", p.ID), &api.CreateProjectColumnOption{
Title: "OrgCol",
Color: "#123456",
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
var col api.ProjectColumn
DecodeJSON(t, resp, &col)
assert.Equal(t, "OrgCol", col.Title)
assert.Equal(t, "#123456", col.Color)
assert.Equal(t, p.ID, col.ProjectID)
// no color
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/org3/projects/%d/columns", p.ID), &api.CreateProjectColumnOption{
Title: "Plain",
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, &col)
assert.Equal(t, "Plain", col.Title)
// empty title
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/org3/projects/%d/columns", p.ID), &api.CreateProjectColumnOption{
Title: "",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
// non-existent project
req = NewRequestWithJSON(t, "POST", "/api/v1/orgs/org3/projects/99999/columns", &api.CreateProjectColumnOption{
Title: "Orphan",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func testAPIOrgUpdateProjectColumn(t *testing.T) {
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
p := makeOrgProject(t, "org3", member.ID)
col := &project_model.Column{
Title: "Orig",
ProjectID: p.ID,
CreatorID: member.ID,
Color: "#000000",
}
assert.NoError(t, project_model.NewColumn(t.Context(), col))
token := getUserToken(t, member.Name, auth_model.AccessTokenScopeWriteIssue)
newTitle := "Changed"
req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/orgs/org3/projects/%d/columns/%d", p.ID, col.ID), &api.EditProjectColumnOption{
Title: &newTitle,
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var got api.ProjectColumn
DecodeJSON(t, resp, &got)
assert.Equal(t, newTitle, got.Title)
newColor := "#FF0000"
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/orgs/org3/projects/%d/columns/%d", p.ID, col.ID), &api.EditProjectColumnOption{
Color: &newColor,
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &got)
assert.Equal(t, newColor, got.Color)
// non-existent column
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/orgs/org3/projects/%d/columns/99999", p.ID), &api.EditProjectColumnOption{
Title: &newTitle,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func testAPIOrgDeleteProjectColumn(t *testing.T) {
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
p := makeOrgProject(t, "org3", member.ID)
col := &project_model.Column{
Title: "ToDelete",
ProjectID: p.ID,
CreatorID: member.ID,
}
assert.NoError(t, project_model.NewColumn(t.Context(), col))
token := getUserToken(t, member.Name, auth_model.AccessTokenScopeWriteIssue)
req := NewRequestf(t, "DELETE", "/api/v1/orgs/org3/projects/%d/columns/%d", p.ID, col.ID).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequestf(t, "DELETE", "/api/v1/orgs/org3/projects/%d/columns/%d", p.ID, col.ID).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func testAPIOrgAddIssueToProjectColumn(t *testing.T) {
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6})
p := makeOrgProject(t, "org3", member.ID)
col1 := &project_model.Column{Title: "Column 1", ProjectID: p.ID, CreatorID: member.ID}
assert.NoError(t, project_model.NewColumn(t.Context(), col1))
col2 := &project_model.Column{Title: "Column 2", ProjectID: p.ID, CreatorID: member.ID}
assert.NoError(t, project_model.NewColumn(t.Context(), col2))
token := getUserToken(t, member.Name, auth_model.AccessTokenScopeWriteIssue)
// add to col1
req := NewRequestWithJSON(t, "POST",
fmt.Sprintf("/api/v1/orgs/org3/projects/%d/columns/%d/issues/%d", p.ID, col1.ID, issue.ID), nil,
).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
pi := unittest.AssertExistsAndLoadBean(t, &project_model.ProjectIssue{ProjectID: p.ID, IssueID: issue.ID})
assert.Equal(t, col1.ID, pi.ProjectColumnID)
// move to col2 via POST
req = NewRequestWithJSON(t, "POST",
fmt.Sprintf("/api/v1/orgs/org3/projects/%d/columns/%d/issues/%d", p.ID, col2.ID, issue.ID), nil,
).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
pi = unittest.AssertExistsAndLoadBean(t, &project_model.ProjectIssue{ProjectID: p.ID, IssueID: issue.ID})
assert.Equal(t, col2.ID, pi.ProjectColumnID)
// idempotent: add to same column again
req = NewRequestWithJSON(t, "POST",
fmt.Sprintf("/api/v1/orgs/org3/projects/%d/columns/%d/issues/%d", p.ID, col2.ID, issue.ID), nil,
).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
// non-existent issue
req = NewRequestWithJSON(t, "POST",
fmt.Sprintf("/api/v1/orgs/org3/projects/%d/columns/%d/issues/99999", p.ID, col1.ID), nil,
).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
// non-existent column
req = NewRequestWithJSON(t, "POST",
fmt.Sprintf("/api/v1/orgs/org3/projects/%d/columns/99999/issues/%d", p.ID, issue.ID), nil,
).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func testAPIOrgRemoveIssueFromProjectColumn(t *testing.T) {
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6})
p := makeOrgProject(t, "org3", member.ID)
col := &project_model.Column{Title: "Col", ProjectID: p.ID, CreatorID: member.ID}
assert.NoError(t, project_model.NewColumn(t.Context(), col))
otherCol := &project_model.Column{Title: "Other", ProjectID: p.ID, CreatorID: member.ID}
assert.NoError(t, project_model.NewColumn(t.Context(), otherCol))
assert.NoError(t, issues_model.IssueAssignOrRemoveProject(t.Context(), issue, member, []int64{p.ID}))
assert.NoError(t, projects_service.MoveIssuesOnProjectColumn(t.Context(), member, col, map[int64]int64{0: issue.ID}))
token := getUserToken(t, member.Name, auth_model.AccessTokenScopeWriteIssue)
// removing via the wrong column must 404 and not detach the issue
req := NewRequestWithJSON(t, "DELETE",
fmt.Sprintf("/api/v1/orgs/org3/projects/%d/columns/%d/issues/%d", p.ID, otherCol.ID, issue.ID), nil,
).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
unittest.AssertExistsAndLoadBean(t, &project_model.ProjectIssue{ProjectID: p.ID, IssueID: issue.ID})
// correct column fully detaches the issue from the project
req = NewRequestWithJSON(t, "DELETE",
fmt.Sprintf("/api/v1/orgs/org3/projects/%d/columns/%d/issues/%d", p.ID, col.ID, issue.ID), nil,
).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
unittest.AssertNotExistsBean(t, &project_model.ProjectIssue{ProjectID: p.ID, IssueID: issue.ID})
}
func testAPIOrgListProjectColumnIssues(t *testing.T) {
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
issueA := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6})
issueB := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 12})
p := makeOrgProject(t, "org3", member.ID)
colA := &project_model.Column{Title: "ColA", ProjectID: p.ID, CreatorID: member.ID}
assert.NoError(t, project_model.NewColumn(t.Context(), colA))
colB := &project_model.Column{Title: "ColB", ProjectID: p.ID, CreatorID: member.ID}
assert.NoError(t, project_model.NewColumn(t.Context(), colB))
assert.NoError(t, issues_model.IssueAssignOrRemoveProject(t.Context(), issueA, member, []int64{p.ID}))
assert.NoError(t, projects_service.MoveIssuesOnProjectColumn(t.Context(), member, colA, map[int64]int64{0: issueA.ID}))
assert.NoError(t, issues_model.IssueAssignOrRemoveProject(t.Context(), issueB, member, []int64{p.ID}))
assert.NoError(t, projects_service.MoveIssuesOnProjectColumn(t.Context(), member, colB, map[int64]int64{0: issueB.ID}))
token := getUserToken(t, member.Name, auth_model.AccessTokenScopeReadIssue)
// colA contains only issueA
req := NewRequestf(t, "GET", "/api/v1/orgs/org3/projects/%d/columns/%d/issues", p.ID, colA.ID).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var gotA []api.Issue
DecodeJSON(t, resp, &gotA)
assert.Len(t, gotA, 1)
assert.Equal(t, issueA.ID, gotA[0].ID)
// colB contains only issueB
req = NewRequestf(t, "GET", "/api/v1/orgs/org3/projects/%d/columns/%d/issues", p.ID, colB.ID).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var gotB []api.Issue
DecodeJSON(t, resp, &gotB)
assert.Len(t, gotB, 1)
assert.Equal(t, issueB.ID, gotB[0].ID)
}
func testAPIOrgMoveProjectIssue(t *testing.T) {
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6})
p := makeOrgProject(t, "org3", member.ID)
colA := &project_model.Column{Title: "A", ProjectID: p.ID, CreatorID: member.ID}
assert.NoError(t, project_model.NewColumn(t.Context(), colA))
colB := &project_model.Column{Title: "B", ProjectID: p.ID, CreatorID: member.ID}
assert.NoError(t, project_model.NewColumn(t.Context(), colB))
assert.NoError(t, issues_model.IssueAssignOrRemoveProject(t.Context(), issue, member, []int64{p.ID}))
assert.NoError(t, projects_service.MoveIssuesOnProjectColumn(t.Context(), member, colA, map[int64]int64{0: issue.ID}))
token := getUserToken(t, member.Name, auth_model.AccessTokenScopeWriteIssue)
// move to colB
req := NewRequestWithJSON(t, "POST",
fmt.Sprintf("/api/v1/orgs/org3/projects/%d/issues/%d/move", p.ID, issue.ID),
&api.MoveProjectIssueOption{ColumnID: colB.ID},
).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
pi := unittest.AssertExistsAndLoadBean(t, &project_model.ProjectIssue{ProjectID: p.ID, IssueID: issue.ID})
assert.Equal(t, colB.ID, pi.ProjectColumnID)
// non-existent target column -> 422
req = NewRequestWithJSON(t, "POST",
fmt.Sprintf("/api/v1/orgs/org3/projects/%d/issues/%d/move", p.ID, issue.ID),
&api.MoveProjectIssueOption{ColumnID: 99999},
).AddTokenAuth(token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
// issue not in project -> 404
otherIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 15})
req = NewRequestWithJSON(t, "POST",
fmt.Sprintf("/api/v1/orgs/org3/projects/%d/issues/%d/move", p.ID, otherIssue.ID),
&api.MoveProjectIssueOption{ColumnID: colA.ID},
).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func testAPIOrgProjectPermissions(t *testing.T) {
// org3 members per fixtures: user2 (id=2), user4 (id=4). Non-member: user5.
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
nonMember := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user5"})
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
p := makeOrgProject(t, "org3", member.ID)
memberToken := getUserToken(t, member.Name, auth_model.AccessTokenScopeWriteIssue)
nmToken := getUserToken(t, nonMember.Name, auth_model.AccessTokenScopeWriteIssue)
adminToken := getUserToken(t, admin.Name, auth_model.AccessTokenScopeWriteIssue)
// anon can list
req := NewRequest(t, "GET", "/api/v1/orgs/org3/projects")
MakeRequest(t, req, http.StatusOK)
// member can read
req = NewRequestf(t, "GET", "/api/v1/orgs/org3/projects/%d", p.ID).AddTokenAuth(memberToken)
MakeRequest(t, req, http.StatusOK)
// non-member cannot write
newTitle := "By NonMember"
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/orgs/org3/projects/%d", p.ID), &api.EditProjectOption{
Title: &newTitle,
}).AddTokenAuth(nmToken)
MakeRequest(t, req, http.StatusForbidden)
// non-member cannot delete
req = NewRequestf(t, "DELETE", "/api/v1/orgs/org3/projects/%d", p.ID).AddTokenAuth(nmToken)
MakeRequest(t, req, http.StatusForbidden)
// non-member cannot create column
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/org3/projects/%d/columns", p.ID), &api.CreateProjectColumnOption{
Title: "By NonMember",
}).AddTokenAuth(nmToken)
MakeRequest(t, req, http.StatusForbidden)
// member can write
newTitle = "By Member"
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/orgs/org3/projects/%d", p.ID), &api.EditProjectOption{
Title: &newTitle,
}).AddTokenAuth(memberToken)
MakeRequest(t, req, http.StatusOK)
// admin can write
newTitle = "By Admin"
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/orgs/org3/projects/%d", p.ID), &api.EditProjectOption{
Title: &newTitle,
}).AddTokenAuth(adminToken)
MakeRequest(t, req, http.StatusOK)
}