// 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) }