// 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" issue_service "code.gitea.io/gitea/services/issue" 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) // Close issueA, then exercise the state filter (issue #4). assert.NoError(t, issue_service.CloseIssue(t.Context(), issueA, member, "")) // default (state omitted) -> open only -> colA returns nothing 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 openOnly []api.Issue DecodeJSON(t, resp, &openOnly) assert.Empty(t, openOnly) // state=closed -> colA returns issueA req = NewRequestf(t, "GET", "/api/v1/orgs/org3/projects/%d/columns/%d/issues?state=closed", p.ID, colA.ID).AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) var closedOnly []api.Issue DecodeJSON(t, resp, &closedOnly) assert.Len(t, closedOnly, 1) assert.Equal(t, issueA.ID, closedOnly[0].ID) // state=all -> colA returns issueA req = NewRequestf(t, "GET", "/api/v1/orgs/org3/projects/%d/columns/%d/issues?state=all", p.ID, colA.ID).AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) var all []api.Issue DecodeJSON(t, resp, &all) assert.Len(t, all, 1) // Columns endpoint populates num_issues / num_open_issues / num_closed_issues (issue #5). req = NewRequestf(t, "GET", "/api/v1/orgs/org3/projects/%d/columns", p.ID).AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) var listed []*api.ProjectColumn DecodeJSON(t, resp, &listed) byID := map[int64]*api.ProjectColumn{} for _, c := range listed { byID[c.ID] = c } if assert.NotNil(t, byID[colA.ID]) { assert.EqualValues(t, 1, byID[colA.ID].NumIssues) assert.EqualValues(t, 0, byID[colA.ID].NumOpenIssues) assert.EqualValues(t, 1, byID[colA.ID].NumClosedIssues) } if assert.NotNil(t, byID[colB.ID]) { assert.EqualValues(t, 1, byID[colB.ID].NumIssues) assert.EqualValues(t, 1, byID[colB.ID].NumOpenIssues) assert.EqualValues(t, 0, byID[colB.ID].NumClosedIssues) } } 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) }