mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-03 21:12:09 +02:00
Merge branch 'fix/project-board-api-review-feedback' of github.com:hanism01/gitea into hanism01-fix/project-board-api-review-feedback
This commit is contained in:
commit
e583e569f2
@ -257,6 +257,23 @@ func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) {
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
// CountColumns returns the total number of columns for a project
|
||||
func (p *Project) CountColumns(ctx context.Context) (int64, error) {
|
||||
return db.GetEngine(ctx).Where("project_id=?", p.ID).Count(&Column{})
|
||||
}
|
||||
|
||||
// GetColumnsPaginated fetches a page of columns for a project
|
||||
func (p *Project) GetColumnsPaginated(ctx context.Context, opts db.ListOptions) (ColumnList, error) {
|
||||
columns := make([]*Column, 0, opts.PageSize)
|
||||
if err := db.SetSessionPagination(db.GetEngine(ctx), &opts).
|
||||
Where("project_id=?", p.ID).
|
||||
OrderBy("sorting, id").
|
||||
Find(&columns); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
// getDefaultColumnWithFallback return default column if one exists
|
||||
// otherwise return the first column by sorting and set it as default column
|
||||
func (p *Project) getDefaultColumnWithFallback(ctx context.Context) (*Column, error) {
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -123,3 +124,39 @@ func Test_NewColumn(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "maximum number of columns reached")
|
||||
}
|
||||
|
||||
func TestCountColumns(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
project, err := GetProjectByID(t.Context(), 1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
count, err := project.CountColumns(t.Context())
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 3, count)
|
||||
}
|
||||
|
||||
func TestGetColumnsPaginated(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
project, err := GetProjectByID(t.Context(), 1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Page 1, limit 2 — returns first 2 columns
|
||||
page1, err := project.GetColumnsPaginated(t.Context(), db.ListOptions{Page: 1, PageSize: 2})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, page1, 2)
|
||||
|
||||
// Page 2, limit 2 — returns remaining column
|
||||
page2, err := project.GetColumnsPaginated(t.Context(), db.ListOptions{Page: 2, PageSize: 2})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, page2, 1)
|
||||
|
||||
// Page 1 and page 2 together cover all columns with no overlap
|
||||
allIDs := make(map[int64]bool)
|
||||
for _, c := range append(page1, page2...) {
|
||||
assert.False(t, allIDs[c.ID], "duplicate column ID %d across pages", c.ID)
|
||||
allIDs[c.ID] = true
|
||||
}
|
||||
assert.Len(t, allIDs, 3)
|
||||
}
|
||||
|
||||
131
modules/structs/project.go
Normal file
131
modules/structs/project.go
Normal file
@ -0,0 +1,131 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Project represents a project
|
||||
// swagger:model
|
||||
type Project struct {
|
||||
// Unique identifier of the project
|
||||
ID int64 `json:"id"`
|
||||
// Project title
|
||||
Title string `json:"title"`
|
||||
// Project description
|
||||
Description string `json:"description"`
|
||||
// Owner ID (for organization or user projects)
|
||||
OwnerID int64 `json:"owner_id,omitempty"`
|
||||
// Repository ID (for repository projects)
|
||||
RepoID int64 `json:"repo_id,omitempty"`
|
||||
// Creator ID
|
||||
CreatorID int64 `json:"creator_id"`
|
||||
// Whether the project is closed
|
||||
IsClosed bool `json:"is_closed"`
|
||||
// Template type: 0=none, 1=basic_kanban, 2=bug_triage
|
||||
TemplateType int `json:"template_type"`
|
||||
// Card type: 0=text_only, 1=images_and_text
|
||||
CardType int `json:"card_type"`
|
||||
// Project type: 1=individual, 2=repository, 3=organization
|
||||
Type int `json:"type"`
|
||||
// Number of open issues
|
||||
NumOpenIssues int64 `json:"num_open_issues,omitempty"`
|
||||
// Number of closed issues
|
||||
NumClosedIssues int64 `json:"num_closed_issues,omitempty"`
|
||||
// Total number of issues
|
||||
NumIssues int64 `json:"num_issues,omitempty"`
|
||||
// Created time
|
||||
// swagger:strfmt date-time
|
||||
Created time.Time `json:"created"`
|
||||
// Updated time
|
||||
// swagger:strfmt date-time
|
||||
Updated time.Time `json:"updated"`
|
||||
// Closed time
|
||||
// swagger:strfmt date-time
|
||||
ClosedDate *time.Time `json:"closed_date,omitempty"`
|
||||
// Project URL
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// CreateProjectOption represents options for creating a project
|
||||
// swagger:model
|
||||
type CreateProjectOption struct {
|
||||
// required: true
|
||||
Title string `json:"title" binding:"Required"`
|
||||
// Project description
|
||||
Description string `json:"description"`
|
||||
// Template type: 0=none, 1=basic_kanban, 2=bug_triage
|
||||
TemplateType int `json:"template_type"`
|
||||
// Card type: 0=text_only, 1=images_and_text
|
||||
CardType int `json:"card_type"`
|
||||
}
|
||||
|
||||
// EditProjectOption represents options for editing a project
|
||||
// swagger:model
|
||||
type EditProjectOption struct {
|
||||
// Project title
|
||||
Title *string `json:"title,omitempty"`
|
||||
// Project description
|
||||
Description *string `json:"description,omitempty"`
|
||||
// Card type: 0=text_only, 1=images_and_text
|
||||
CardType *int `json:"card_type,omitempty"`
|
||||
// State of the project (open or closed)
|
||||
State *string `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
// ProjectColumn represents a project column (board)
|
||||
// swagger:model
|
||||
type ProjectColumn struct {
|
||||
// Unique identifier of the column
|
||||
ID int64 `json:"id"`
|
||||
// Column title
|
||||
Title string `json:"title"`
|
||||
// Whether this is the default column
|
||||
Default bool `json:"default"`
|
||||
// Sorting order
|
||||
Sorting int `json:"sorting"`
|
||||
// Column color (hex format)
|
||||
Color string `json:"color,omitempty"`
|
||||
// Project ID
|
||||
ProjectID int64 `json:"project_id"`
|
||||
// Creator ID
|
||||
CreatorID int64 `json:"creator_id"`
|
||||
// Number of issues in this column
|
||||
NumIssues int64 `json:"num_issues,omitempty"`
|
||||
// Created time
|
||||
// swagger:strfmt date-time
|
||||
Created time.Time `json:"created"`
|
||||
// Updated time
|
||||
// swagger:strfmt date-time
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
// CreateProjectColumnOption represents options for creating a project column
|
||||
// swagger:model
|
||||
type CreateProjectColumnOption struct {
|
||||
// required: true
|
||||
Title string `json:"title" binding:"Required"`
|
||||
// Column color (hex format, e.g., #FF0000)
|
||||
Color string `json:"color,omitempty"`
|
||||
}
|
||||
|
||||
// EditProjectColumnOption represents options for editing a project column
|
||||
// swagger:model
|
||||
type EditProjectColumnOption struct {
|
||||
// Column title
|
||||
Title *string `json:"title,omitempty"`
|
||||
// Column color (hex format)
|
||||
Color *string `json:"color,omitempty"`
|
||||
// Sorting order
|
||||
Sorting *int `json:"sorting,omitempty"`
|
||||
}
|
||||
|
||||
// AddIssueToProjectColumnOption represents options for adding an issue to a project column
|
||||
// swagger:model
|
||||
type AddIssueToProjectColumnOption struct {
|
||||
// Issue ID to add to the column
|
||||
// required: true
|
||||
IssueID int64 `json:"issue_id" binding:"Required"`
|
||||
}
|
||||
@ -1571,6 +1571,23 @@ func Routes() *web.Router {
|
||||
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
|
||||
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
|
||||
})
|
||||
m.Group("/projects", func() {
|
||||
m.Combo("").Get(repo.ListProjects).
|
||||
Post(reqToken(), reqRepoWriter(unit.TypeProjects), mustNotBeArchived, bind(api.CreateProjectOption{}), repo.CreateProject)
|
||||
m.Group("/{id}", func() {
|
||||
m.Combo("").Get(repo.GetProject).
|
||||
Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectOption{}), repo.EditProject).
|
||||
Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProject)
|
||||
m.Combo("/columns").Get(repo.ListProjectColumns).
|
||||
Post(reqToken(), reqRepoWriter(unit.TypeProjects), mustNotBeArchived, bind(api.CreateProjectColumnOption{}), repo.CreateProjectColumn)
|
||||
})
|
||||
m.Group("/columns/{id}", func() {
|
||||
m.Combo("").
|
||||
Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectColumnOption{}), repo.EditProjectColumn).
|
||||
Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProjectColumn)
|
||||
m.Post("/issues", reqToken(), reqRepoWriter(unit.TypeProjects), mustNotBeArchived, bind(api.AddIssueToProjectColumnOption{}), repo.AddIssueToProjectColumn)
|
||||
})
|
||||
}, reqRepoReader(unit.TypeProjects))
|
||||
}, repoAssignment(), checkTokenPublicOnly())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
|
||||
|
||||
|
||||
643
routers/api/v1/repo/project.go
Normal file
643
routers/api/v1/repo/project.go
Normal file
@ -0,0 +1,643 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
project_service "code.gitea.io/gitea/services/projects"
|
||||
)
|
||||
|
||||
func getRepoProjectByID(ctx *context.APIContext) *project_model.Project {
|
||||
project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return project
|
||||
}
|
||||
|
||||
func getRepoProjectColumn(ctx *context.APIContext) *project_model.Column {
|
||||
column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectColumnNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
_, err = project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, column.ProjectID)
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return column
|
||||
}
|
||||
|
||||
// ListProjects lists all projects in a repository
|
||||
func ListProjects(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/projects repository repoListProjects
|
||||
// ---
|
||||
// summary: List projects in a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: state
|
||||
// in: query
|
||||
// description: State of the project (open, closed)
|
||||
// type: string
|
||||
// enum: [open, closed, all]
|
||||
// default: open
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ProjectList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
state := ctx.FormTrim("state")
|
||||
var isClosed optional.Option[bool]
|
||||
switch state {
|
||||
case "closed":
|
||||
isClosed = optional.Some(true)
|
||||
case "open":
|
||||
isClosed = optional.Some(false)
|
||||
case "all":
|
||||
isClosed = optional.None[bool]()
|
||||
default:
|
||||
isClosed = optional.Some(false)
|
||||
}
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: listOptions,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsClosed: isClosed,
|
||||
Type: project_model.TypeRepository,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiProjects := convert.ToProjectList(ctx, projects)
|
||||
|
||||
ctx.SetLinkHeader(count, listOptions.PageSize)
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, apiProjects)
|
||||
}
|
||||
|
||||
// GetProject gets a single project
|
||||
func GetProject(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/projects/{id} repository repoGetProject
|
||||
// ---
|
||||
// summary: Get a single project
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the project
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Project"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
project := getRepoProjectByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_service.LoadIssueNumbersForProject(ctx, project, ctx.Doer); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToProject(ctx, project))
|
||||
}
|
||||
|
||||
// CreateProject creates a new project
|
||||
func CreateProject(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/projects repository repoCreateProject
|
||||
// ---
|
||||
// summary: Create a new project
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateProjectOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Project"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateProjectOption)
|
||||
|
||||
p := &project_model.Project{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Title: form.Title,
|
||||
Description: form.Description,
|
||||
CreatorID: ctx.Doer.ID,
|
||||
TemplateType: project_model.TemplateType(form.TemplateType),
|
||||
CardType: project_model.CardType(form.CardType),
|
||||
Type: project_model.TypeRepository,
|
||||
}
|
||||
|
||||
if err := project_model.NewProject(ctx, p); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToProject(ctx, p))
|
||||
}
|
||||
|
||||
// EditProject updates a project
|
||||
func EditProject(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/projects/{id} repository repoEditProject
|
||||
// ---
|
||||
// summary: Edit a project
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the project
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditProjectOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Project"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
project := getRepoProjectByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditProjectOption)
|
||||
|
||||
if form.Title != nil {
|
||||
project.Title = *form.Title
|
||||
}
|
||||
if form.Description != nil {
|
||||
project.Description = *form.Description
|
||||
}
|
||||
if form.CardType != nil {
|
||||
project.CardType = project_model.CardType(*form.CardType)
|
||||
}
|
||||
if err := project_model.UpdateProject(ctx, project); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if form.State != nil {
|
||||
isClosed := *form.State == string(api.StateClosed)
|
||||
if isClosed != project.IsClosed {
|
||||
if err := project_model.ChangeProjectStatus(ctx, project, isClosed); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := project_service.LoadIssueNumbersForProject(ctx, project, ctx.Doer); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToProject(ctx, project))
|
||||
}
|
||||
|
||||
// DeleteProject deletes a project
|
||||
func DeleteProject(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/projects/{id} repository repoDeleteProject
|
||||
// ---
|
||||
// summary: Delete a project
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the project
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
project := getRepoProjectByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_model.DeleteProjectByID(ctx, project.ID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ListProjectColumns lists all columns in a project
|
||||
func ListProjectColumns(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/projects/{id}/columns repository repoListProjectColumns
|
||||
// ---
|
||||
// summary: List columns in a project
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the project
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ProjectColumnList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
project := getRepoProjectByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
total, err := project.CountColumns(ctx)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
columns, err := project.GetColumnsPaginated(ctx, listOptions)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(total, listOptions.PageSize)
|
||||
ctx.SetTotalCountHeader(total)
|
||||
ctx.JSON(http.StatusOK, convert.ToProjectColumnList(ctx, columns))
|
||||
}
|
||||
|
||||
// CreateProjectColumn creates a new column in a project
|
||||
func CreateProjectColumn(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/columns repository repoCreateProjectColumn
|
||||
// ---
|
||||
// summary: Create a new column in a project
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the project
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateProjectColumnOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/ProjectColumn"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
project := getRepoProjectByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateProjectColumnOption)
|
||||
|
||||
column := &project_model.Column{
|
||||
Title: form.Title,
|
||||
Color: form.Color,
|
||||
ProjectID: project.ID,
|
||||
CreatorID: ctx.Doer.ID,
|
||||
}
|
||||
|
||||
if err := project_model.NewColumn(ctx, column); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToProjectColumn(ctx, column))
|
||||
}
|
||||
|
||||
// EditProjectColumn updates a column
|
||||
func EditProjectColumn(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/projects/columns/{id} repository repoEditProjectColumn
|
||||
// ---
|
||||
// summary: Edit a project column
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the column
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditProjectColumnOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ProjectColumn"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
column := getRepoProjectColumn(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditProjectColumnOption)
|
||||
|
||||
if form.Title != nil {
|
||||
column.Title = *form.Title
|
||||
}
|
||||
if form.Color != nil {
|
||||
column.Color = *form.Color
|
||||
}
|
||||
if form.Sorting != nil {
|
||||
sorting := int8(*form.Sorting)
|
||||
if int(sorting) != *form.Sorting {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "sorting out of range")
|
||||
return
|
||||
}
|
||||
column.Sorting = sorting
|
||||
}
|
||||
|
||||
if err := project_model.UpdateColumn(ctx, column); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToProjectColumn(ctx, column))
|
||||
}
|
||||
|
||||
// DeleteProjectColumn deletes a column
|
||||
func DeleteProjectColumn(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/projects/columns/{id} repository repoDeleteProjectColumn
|
||||
// ---
|
||||
// summary: Delete a project column
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the column
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
column := getRepoProjectColumn(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_model.DeleteColumnByID(ctx, column.ID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// AddIssueToProjectColumn adds an issue to a project column
|
||||
func AddIssueToProjectColumn(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/projects/columns/{id}/issues repository repoAddIssueToProjectColumn
|
||||
// ---
|
||||
// summary: Add an issue to a project column
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the column
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/AddIssueToProjectColumnOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
column := getRepoProjectColumn(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.AddIssueToProjectColumnOption)
|
||||
|
||||
issue, err := issues_model.GetIssueByID(ctx, form.IssueID)
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "issue not found")
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "issue does not belong to this repository")
|
||||
return
|
||||
}
|
||||
|
||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, column.ProjectID, column.ID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
@ -230,4 +230,17 @@ type swaggerParameterBodies struct {
|
||||
|
||||
// in:body
|
||||
LockIssueOption api.LockIssueOption
|
||||
|
||||
// in:body
|
||||
CreateProjectOption api.CreateProjectOption
|
||||
// in:body
|
||||
EditProjectOption api.EditProjectOption
|
||||
|
||||
// in:body
|
||||
CreateProjectColumnOption api.CreateProjectColumnOption
|
||||
// in:body
|
||||
EditProjectColumnOption api.EditProjectColumnOption
|
||||
|
||||
// in:body
|
||||
AddIssueToProjectColumnOption api.AddIssueToProjectColumnOption
|
||||
}
|
||||
|
||||
36
routers/api/v1/swagger/project.go
Normal file
36
routers/api/v1/swagger/project.go
Normal file
@ -0,0 +1,36 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package swagger
|
||||
|
||||
import (
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
)
|
||||
|
||||
// Project
|
||||
// swagger:response Project
|
||||
type swaggerResponseProject struct {
|
||||
// in:body
|
||||
Body api.Project `json:"body"`
|
||||
}
|
||||
|
||||
// ProjectList
|
||||
// swagger:response ProjectList
|
||||
type swaggerResponseProjectList struct {
|
||||
// in:body
|
||||
Body []api.Project `json:"body"`
|
||||
}
|
||||
|
||||
// ProjectColumn
|
||||
// swagger:response ProjectColumn
|
||||
type swaggerResponseProjectColumn struct {
|
||||
// in:body
|
||||
Body api.ProjectColumn `json:"body"`
|
||||
}
|
||||
|
||||
// ProjectColumnList
|
||||
// swagger:response ProjectColumnList
|
||||
type swaggerResponseProjectColumnList struct {
|
||||
// in:body
|
||||
Body []api.ProjectColumn `json:"body"`
|
||||
}
|
||||
92
services/convert/project.go
Normal file
92
services/convert/project.go
Normal file
@ -0,0 +1,92 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package convert
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
)
|
||||
|
||||
// ToProject converts a project_model.Project to api.Project
|
||||
func ToProject(ctx context.Context, p *project_model.Project) *api.Project {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
project := &api.Project{
|
||||
ID: p.ID,
|
||||
Title: p.Title,
|
||||
Description: p.Description,
|
||||
OwnerID: p.OwnerID,
|
||||
RepoID: p.RepoID,
|
||||
CreatorID: p.CreatorID,
|
||||
IsClosed: p.IsClosed,
|
||||
TemplateType: int(p.TemplateType),
|
||||
CardType: int(p.CardType),
|
||||
Type: int(p.Type),
|
||||
NumOpenIssues: p.NumOpenIssues,
|
||||
NumClosedIssues: p.NumClosedIssues,
|
||||
NumIssues: p.NumIssues,
|
||||
Created: p.CreatedUnix.AsTime(),
|
||||
Updated: p.UpdatedUnix.AsTime(),
|
||||
}
|
||||
|
||||
if p.ClosedDateUnix > 0 {
|
||||
t := p.ClosedDateUnix.AsTime()
|
||||
project.ClosedDate = &t
|
||||
}
|
||||
|
||||
// Generate project URL
|
||||
if p.Type == project_model.TypeRepository && p.RepoID > 0 {
|
||||
if err := p.LoadRepo(ctx); err == nil && p.Repo != nil {
|
||||
project.URL = project_model.ProjectLinkForRepo(p.Repo, p.ID)
|
||||
}
|
||||
} else if p.OwnerID > 0 {
|
||||
if err := p.LoadOwner(ctx); err == nil && p.Owner != nil {
|
||||
project.URL = project_model.ProjectLinkForOrg(p.Owner, p.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return project
|
||||
}
|
||||
|
||||
// ToProjectColumn converts a project_model.Column to api.ProjectColumn
|
||||
func ToProjectColumn(ctx context.Context, column *project_model.Column) *api.ProjectColumn {
|
||||
if column == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &api.ProjectColumn{
|
||||
ID: column.ID,
|
||||
Title: column.Title,
|
||||
Default: column.Default,
|
||||
Sorting: int(column.Sorting),
|
||||
Color: column.Color,
|
||||
ProjectID: column.ProjectID,
|
||||
CreatorID: column.CreatorID,
|
||||
NumIssues: column.NumIssues,
|
||||
Created: column.CreatedUnix.AsTime(),
|
||||
Updated: column.UpdatedUnix.AsTime(),
|
||||
}
|
||||
}
|
||||
|
||||
// ToProjectList converts a list of project_model.Project to a list of api.Project
|
||||
func ToProjectList(ctx context.Context, projects []*project_model.Project) []*api.Project {
|
||||
result := make([]*api.Project, len(projects))
|
||||
for i, p := range projects {
|
||||
result[i] = ToProject(ctx, p)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ToProjectColumnList converts a list of project_model.Column to a list of api.ProjectColumn
|
||||
func ToProjectColumnList(ctx context.Context, columns []*project_model.Column) []*api.ProjectColumn {
|
||||
result := make([]*api.ProjectColumn, len(columns))
|
||||
for i, column := range columns {
|
||||
result[i] = ToProjectColumn(ctx, column)
|
||||
}
|
||||
return result
|
||||
}
|
||||
830
templates/swagger/v1_json.tmpl
generated
830
templates/swagger/v1_json.tmpl
generated
@ -13743,6 +13743,518 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/projects": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "List projects in a repository",
|
||||
"operationId": "repoListProjects",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"open",
|
||||
"closed",
|
||||
"all"
|
||||
],
|
||||
"type": "string",
|
||||
"default": "open",
|
||||
"description": "State of the project (open, closed)",
|
||||
"name": "state",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "page number of results",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "page size of results",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/ProjectList"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Create a new project",
|
||||
"operationId": "repoCreateProject",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/CreateProjectOption"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"$ref": "#/responses/Project"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#/responses/validationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/projects/columns/{id}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Delete a project column",
|
||||
"operationId": "repoDeleteProjectColumn",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "id of the column",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/responses/empty"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Edit a project column",
|
||||
"operationId": "repoEditProjectColumn",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "id of the column",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/EditProjectColumnOption"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/ProjectColumn"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#/responses/validationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/projects/columns/{id}/issues": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Add an issue to a project column",
|
||||
"operationId": "repoAddIssueToProjectColumn",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "id of the column",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/AddIssueToProjectColumnOption"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"$ref": "#/responses/empty"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/responses/forbidden"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#/responses/validationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/projects/{id}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Get a single project",
|
||||
"operationId": "repoGetProject",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "id of the project",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/Project"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Delete a project",
|
||||
"operationId": "repoDeleteProject",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "id of the project",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/responses/empty"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Edit a project",
|
||||
"operationId": "repoEditProject",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "id of the project",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/EditProjectOption"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/Project"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#/responses/validationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/projects/{id}/columns": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "List columns in a project",
|
||||
"operationId": "repoListProjectColumns",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "id of the project",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "page number of results to return (1-based)",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "page size of results",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/ProjectColumnList"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Create a new column in a project",
|
||||
"operationId": "repoCreateProjectColumn",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "id of the project",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/CreateProjectColumnOption"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"$ref": "#/responses/ProjectColumn"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#/responses/validationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/pulls": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@ -22027,6 +22539,22 @@
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"AddIssueToProjectColumnOption": {
|
||||
"description": "AddIssueToProjectColumnOption represents options for adding an issue to a project column",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"issue_id"
|
||||
],
|
||||
"properties": {
|
||||
"issue_id": {
|
||||
"description": "Issue ID to add to the column",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "IssueID"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"AddTimeOption": {
|
||||
"description": "AddTimeOption options for adding time to an issue",
|
||||
"type": "object",
|
||||
@ -23765,6 +24293,56 @@
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"CreateProjectColumnOption": {
|
||||
"description": "CreateProjectColumnOption represents options for creating a project column",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "Column color (hex format, e.g., #FF0000)",
|
||||
"type": "string",
|
||||
"x-go-name": "Color"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"x-go-name": "Title"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"CreateProjectOption": {
|
||||
"description": "CreateProjectOption represents options for creating a project",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"title"
|
||||
],
|
||||
"properties": {
|
||||
"card_type": {
|
||||
"description": "Card type: 0=text_only, 1=images_and_text",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "CardType"
|
||||
},
|
||||
"description": {
|
||||
"description": "Project description",
|
||||
"type": "string",
|
||||
"x-go-name": "Description"
|
||||
},
|
||||
"template_type": {
|
||||
"description": "Template type: 0=none, 1=basic_kanban, 2=bug_triage",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "TemplateType"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"x-go-name": "Title"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"CreatePullRequestOption": {
|
||||
"description": "CreatePullRequestOption options when creating a pull request",
|
||||
"type": "object",
|
||||
@ -24923,6 +25501,57 @@
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"EditProjectColumnOption": {
|
||||
"description": "EditProjectColumnOption represents options for editing a project column",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "Column color (hex format)",
|
||||
"type": "string",
|
||||
"x-go-name": "Color"
|
||||
},
|
||||
"sorting": {
|
||||
"description": "Sorting order",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "Sorting"
|
||||
},
|
||||
"title": {
|
||||
"description": "Column title",
|
||||
"type": "string",
|
||||
"x-go-name": "Title"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"EditProjectOption": {
|
||||
"description": "EditProjectOption represents options for editing a project",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"card_type": {
|
||||
"description": "Card type: 0=text_only, 1=images_and_text",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "CardType"
|
||||
},
|
||||
"description": {
|
||||
"description": "Project description",
|
||||
"type": "string",
|
||||
"x-go-name": "Description"
|
||||
},
|
||||
"state": {
|
||||
"description": "State of the project (open or closed)",
|
||||
"type": "string",
|
||||
"x-go-name": "State"
|
||||
},
|
||||
"title": {
|
||||
"description": "Project title",
|
||||
"type": "string",
|
||||
"x-go-name": "Title"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"EditPullRequestOption": {
|
||||
"description": "EditPullRequestOption options when modify pull request",
|
||||
"type": "object",
|
||||
@ -27651,6 +28280,175 @@
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"Project": {
|
||||
"description": "Project represents a project",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"card_type": {
|
||||
"description": "Card type: 0=text_only, 1=images_and_text",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "CardType"
|
||||
},
|
||||
"closed_date": {
|
||||
"description": "Closed time",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "ClosedDate"
|
||||
},
|
||||
"created": {
|
||||
"description": "Created time",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "Created"
|
||||
},
|
||||
"creator_id": {
|
||||
"description": "Creator ID",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "CreatorID"
|
||||
},
|
||||
"description": {
|
||||
"description": "Project description",
|
||||
"type": "string",
|
||||
"x-go-name": "Description"
|
||||
},
|
||||
"id": {
|
||||
"description": "Unique identifier of the project",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "ID"
|
||||
},
|
||||
"is_closed": {
|
||||
"description": "Whether the project is closed",
|
||||
"type": "boolean",
|
||||
"x-go-name": "IsClosed"
|
||||
},
|
||||
"num_closed_issues": {
|
||||
"description": "Number of closed issues",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "NumClosedIssues"
|
||||
},
|
||||
"num_issues": {
|
||||
"description": "Total number of issues",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "NumIssues"
|
||||
},
|
||||
"num_open_issues": {
|
||||
"description": "Number of open issues",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "NumOpenIssues"
|
||||
},
|
||||
"owner_id": {
|
||||
"description": "Owner ID (for organization or user projects)",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "OwnerID"
|
||||
},
|
||||
"repo_id": {
|
||||
"description": "Repository ID (for repository projects)",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "RepoID"
|
||||
},
|
||||
"template_type": {
|
||||
"description": "Template type: 0=none, 1=basic_kanban, 2=bug_triage",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "TemplateType"
|
||||
},
|
||||
"title": {
|
||||
"description": "Project title",
|
||||
"type": "string",
|
||||
"x-go-name": "Title"
|
||||
},
|
||||
"type": {
|
||||
"description": "Project type: 1=individual, 2=repository, 3=organization",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "Type"
|
||||
},
|
||||
"updated": {
|
||||
"description": "Updated time",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "Updated"
|
||||
},
|
||||
"url": {
|
||||
"description": "Project URL",
|
||||
"type": "string",
|
||||
"x-go-name": "URL"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"ProjectColumn": {
|
||||
"description": "ProjectColumn represents a project column (board)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"description": "Column color (hex format)",
|
||||
"type": "string",
|
||||
"x-go-name": "Color"
|
||||
},
|
||||
"created": {
|
||||
"description": "Created time",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "Created"
|
||||
},
|
||||
"creator_id": {
|
||||
"description": "Creator ID",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "CreatorID"
|
||||
},
|
||||
"default": {
|
||||
"description": "Whether this is the default column",
|
||||
"type": "boolean",
|
||||
"x-go-name": "Default"
|
||||
},
|
||||
"id": {
|
||||
"description": "Unique identifier of the column",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "ID"
|
||||
},
|
||||
"num_issues": {
|
||||
"description": "Number of issues in this column",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "NumIssues"
|
||||
},
|
||||
"project_id": {
|
||||
"description": "Project ID",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "ProjectID"
|
||||
},
|
||||
"sorting": {
|
||||
"description": "Sorting order",
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "Sorting"
|
||||
},
|
||||
"title": {
|
||||
"description": "Column title",
|
||||
"type": "string",
|
||||
"x-go-name": "Title"
|
||||
},
|
||||
"updated": {
|
||||
"description": "Updated time",
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "Updated"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"PublicKey": {
|
||||
"description": "PublicKey publickey is a user key to push code to repository",
|
||||
"type": "object",
|
||||
@ -30464,6 +31262,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Project": {
|
||||
"description": "Project",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Project"
|
||||
}
|
||||
},
|
||||
"ProjectColumn": {
|
||||
"description": "ProjectColumn",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ProjectColumn"
|
||||
}
|
||||
},
|
||||
"ProjectColumnList": {
|
||||
"description": "ProjectColumnList",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ProjectColumn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProjectList": {
|
||||
"description": "ProjectList",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Project"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PublicKey": {
|
||||
"description": "PublicKey",
|
||||
"schema": {
|
||||
@ -30931,7 +31759,7 @@
|
||||
"parameterBodies": {
|
||||
"description": "parameterBodies",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/LockIssueOption"
|
||||
"$ref": "#/definitions/AddIssueToProjectColumnOption"
|
||||
}
|
||||
},
|
||||
"redirect": {
|
||||
|
||||
632
tests/integration/api_repo_project_test.go
Normal file
632
tests/integration/api_repo_project_test.go
Normal file
@ -0,0 +1,632 @@
|
||||
// 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"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIListProjects(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeReadIssue)
|
||||
|
||||
// Test listing all projects
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/projects", owner.Name, repo.Name).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var projects []*api.Project
|
||||
DecodeJSON(t, resp, &projects)
|
||||
|
||||
// Test state filter - open
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/projects?state=open", owner.Name, repo.Name).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &projects)
|
||||
for _, project := range projects {
|
||||
assert.False(t, project.IsClosed, "Project should be open")
|
||||
}
|
||||
|
||||
// Test state filter - all
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/projects?state=all", owner.Name, repo.Name).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &projects)
|
||||
|
||||
// Test pagination
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/projects?page=1&limit=5", owner.Name, repo.Name).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestAPIGetProject(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
// Create a test project
|
||||
project := &project_model.Project{
|
||||
Title: "Test Project for API",
|
||||
RepoID: repo.ID,
|
||||
Type: project_model.TypeRepository,
|
||||
CreatorID: owner.ID,
|
||||
TemplateType: project_model.TemplateTypeNone,
|
||||
}
|
||||
err := project_model.NewProject(t.Context(), project)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
_ = project_model.DeleteProjectByID(t.Context(), project.ID)
|
||||
}()
|
||||
|
||||
token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeReadIssue)
|
||||
|
||||
// Test getting the project
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/projects/%d", owner.Name, repo.Name, project.ID).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var apiProject api.Project
|
||||
DecodeJSON(t, resp, &apiProject)
|
||||
assert.Equal(t, project.Title, apiProject.Title)
|
||||
assert.Equal(t, project.ID, apiProject.ID)
|
||||
assert.Equal(t, repo.ID, apiProject.RepoID)
|
||||
assert.NotEmpty(t, apiProject.URL)
|
||||
|
||||
// Test getting non-existent project
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/projects/99999", owner.Name, repo.Name).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPICreateProject(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// Test creating a project
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/projects", owner.Name, repo.Name), &api.CreateProjectOption{
|
||||
Title: "API Created Project",
|
||||
Description: "This is a test project created via API",
|
||||
TemplateType: 1, // basic_kanban
|
||||
CardType: 1, // images_and_text
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
var project api.Project
|
||||
DecodeJSON(t, resp, &project)
|
||||
assert.Equal(t, "API Created Project", project.Title)
|
||||
assert.Equal(t, "This is a test project created via API", project.Description)
|
||||
assert.Equal(t, 1, project.TemplateType)
|
||||
assert.Equal(t, 1, project.CardType)
|
||||
assert.False(t, project.IsClosed)
|
||||
defer func() {
|
||||
_ = project_model.DeleteProjectByID(t.Context(), project.ID)
|
||||
}()
|
||||
|
||||
// Test creating with minimal data
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/projects", owner.Name, repo.Name), &api.CreateProjectOption{
|
||||
Title: "Minimal Project",
|
||||
}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
var minimalProject api.Project
|
||||
DecodeJSON(t, resp, &minimalProject)
|
||||
assert.Equal(t, "Minimal Project", minimalProject.Title)
|
||||
defer func() {
|
||||
_ = project_model.DeleteProjectByID(t.Context(), minimalProject.ID)
|
||||
}()
|
||||
|
||||
// Test creating without authentication
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/projects", owner.Name, repo.Name), &api.CreateProjectOption{
|
||||
Title: "Unauthorized Project",
|
||||
})
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
// Test creating with invalid data (empty title)
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/projects", owner.Name, repo.Name), &api.CreateProjectOption{
|
||||
Title: "",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
}
|
||||
|
||||
func TestAPIUpdateProject(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
// Create a test project
|
||||
project := &project_model.Project{
|
||||
Title: "Project to Update",
|
||||
Description: "Original description",
|
||||
RepoID: repo.ID,
|
||||
Type: project_model.TypeRepository,
|
||||
CreatorID: owner.ID,
|
||||
TemplateType: project_model.TemplateTypeNone,
|
||||
}
|
||||
err := project_model.NewProject(t.Context(), project)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
_ = project_model.DeleteProjectByID(t.Context(), project.ID)
|
||||
}()
|
||||
|
||||
token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// Test updating project title and description
|
||||
newTitle := "Updated Project Title"
|
||||
newDesc := "Updated description"
|
||||
req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/projects/%d", owner.Name, repo.Name, project.ID), &api.EditProjectOption{
|
||||
Title: &newTitle,
|
||||
Description: &newDesc,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var updatedProject api.Project
|
||||
DecodeJSON(t, resp, &updatedProject)
|
||||
assert.Equal(t, newTitle, updatedProject.Title)
|
||||
assert.Equal(t, newDesc, updatedProject.Description)
|
||||
|
||||
// Test updating non-existent project
|
||||
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/projects/99999", owner.Name, repo.Name), &api.EditProjectOption{
|
||||
Title: &newTitle,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIChangeProjectStatus(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
project := &project_model.Project{
|
||||
Title: "Project to Close",
|
||||
Description: "Project to close and reopen",
|
||||
RepoID: repo.ID,
|
||||
Type: project_model.TypeRepository,
|
||||
CreatorID: owner.ID,
|
||||
TemplateType: project_model.TemplateTypeNone,
|
||||
}
|
||||
err := project_model.NewProject(t.Context(), project)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
_ = project_model.DeleteProjectByID(t.Context(), project.ID)
|
||||
}()
|
||||
|
||||
token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// Close via PATCH with state=closed
|
||||
closed := "closed"
|
||||
req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/projects/%d", owner.Name, repo.Name, project.ID), &api.EditProjectOption{
|
||||
State: &closed,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var updatedProject api.Project
|
||||
DecodeJSON(t, resp, &updatedProject)
|
||||
assert.True(t, updatedProject.IsClosed)
|
||||
assert.NotNil(t, updatedProject.ClosedDate)
|
||||
|
||||
// Reopen via PATCH with state=open
|
||||
open := "open"
|
||||
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/projects/%d", owner.Name, repo.Name, project.ID), &api.EditProjectOption{
|
||||
State: &open,
|
||||
}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
DecodeJSON(t, resp, &updatedProject)
|
||||
assert.False(t, updatedProject.IsClosed)
|
||||
}
|
||||
|
||||
func TestAPIDeleteProject(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
// Create a test project
|
||||
project := &project_model.Project{
|
||||
Title: "Project to Delete",
|
||||
RepoID: repo.ID,
|
||||
Type: project_model.TypeRepository,
|
||||
CreatorID: owner.ID,
|
||||
TemplateType: project_model.TemplateTypeNone,
|
||||
}
|
||||
err := project_model.NewProject(t.Context(), project)
|
||||
assert.NoError(t, err)
|
||||
|
||||
token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// Test deleting the project
|
||||
req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/projects/%d", owner.Name, repo.Name, project.ID).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Test deleting non-existent project (including the one we just deleted)
|
||||
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/projects/%d", owner.Name, repo.Name, project.ID).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIListProjectColumns(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
// Create a test project
|
||||
project := &project_model.Project{
|
||||
Title: "Project for Columns Test",
|
||||
RepoID: repo.ID,
|
||||
Type: project_model.TypeRepository,
|
||||
CreatorID: owner.ID,
|
||||
TemplateType: project_model.TemplateTypeNone,
|
||||
}
|
||||
err := project_model.NewProject(t.Context(), project)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
_ = project_model.DeleteProjectByID(t.Context(), project.ID)
|
||||
}()
|
||||
|
||||
// Create test columns
|
||||
for i := 1; i <= 3; i++ {
|
||||
column := &project_model.Column{
|
||||
Title: fmt.Sprintf("Column %d", i),
|
||||
ProjectID: project.ID,
|
||||
CreatorID: owner.ID,
|
||||
}
|
||||
err = project_model.NewColumn(t.Context(), column)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeReadIssue)
|
||||
|
||||
// Test listing all columns — X-Total-Count must equal 3
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/projects/%d/columns", owner.Name, repo.Name, project.ID).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var columns []*api.ProjectColumn
|
||||
DecodeJSON(t, resp, &columns)
|
||||
assert.Len(t, columns, 3)
|
||||
assert.Equal(t, "Column 1", columns[0].Title)
|
||||
assert.Equal(t, "Column 2", columns[1].Title)
|
||||
assert.Equal(t, "Column 3", columns[2].Title)
|
||||
assert.Equal(t, "3", resp.Header().Get("X-Total-Count"))
|
||||
|
||||
// Test pagination: page 1 with limit 2 returns first 2 columns, total count still 3
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/projects/%d/columns?page=1&limit=2", owner.Name, repo.Name, project.ID).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &columns)
|
||||
assert.Len(t, columns, 2)
|
||||
assert.Equal(t, "Column 1", columns[0].Title)
|
||||
assert.Equal(t, "Column 2", columns[1].Title)
|
||||
assert.Equal(t, "3", resp.Header().Get("X-Total-Count"))
|
||||
|
||||
// Test pagination: page 2 with limit 2 returns remaining column
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/projects/%d/columns?page=2&limit=2", owner.Name, repo.Name, project.ID).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &columns)
|
||||
assert.Len(t, columns, 1)
|
||||
assert.Equal(t, "Column 3", columns[0].Title)
|
||||
assert.Equal(t, "3", resp.Header().Get("X-Total-Count"))
|
||||
|
||||
// Test listing columns for non-existent project
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/projects/99999/columns", owner.Name, repo.Name).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPICreateProjectColumn(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
// Create a test project
|
||||
project := &project_model.Project{
|
||||
Title: "Project for Column Creation",
|
||||
RepoID: repo.ID,
|
||||
Type: project_model.TypeRepository,
|
||||
CreatorID: owner.ID,
|
||||
TemplateType: project_model.TemplateTypeNone,
|
||||
}
|
||||
err := project_model.NewProject(t.Context(), project)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
_ = project_model.DeleteProjectByID(t.Context(), project.ID)
|
||||
}()
|
||||
|
||||
token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// Test creating a column with color
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/projects/%d/columns", owner.Name, repo.Name, project.ID), &api.CreateProjectColumnOption{
|
||||
Title: "New Column",
|
||||
Color: "#FF5733",
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
var column api.ProjectColumn
|
||||
DecodeJSON(t, resp, &column)
|
||||
assert.Equal(t, "New Column", column.Title)
|
||||
assert.Equal(t, "#FF5733", column.Color)
|
||||
assert.Equal(t, project.ID, column.ProjectID)
|
||||
|
||||
// Test creating a column without color
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/projects/%d/columns", owner.Name, repo.Name, project.ID), &api.CreateProjectColumnOption{
|
||||
Title: "Simple Column",
|
||||
}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
DecodeJSON(t, resp, &column)
|
||||
assert.Equal(t, "Simple Column", column.Title)
|
||||
|
||||
// Test creating with empty title
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/projects/%d/columns", owner.Name, repo.Name, project.ID), &api.CreateProjectColumnOption{
|
||||
Title: "",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
|
||||
// Test creating for non-existent project
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/projects/99999/columns", owner.Name, repo.Name), &api.CreateProjectColumnOption{
|
||||
Title: "Orphan Column",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIUpdateProjectColumn(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
// Create a test project and column
|
||||
project := &project_model.Project{
|
||||
Title: "Project for Column Update",
|
||||
RepoID: repo.ID,
|
||||
Type: project_model.TypeRepository,
|
||||
CreatorID: owner.ID,
|
||||
TemplateType: project_model.TemplateTypeNone,
|
||||
}
|
||||
err := project_model.NewProject(t.Context(), project)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
_ = project_model.DeleteProjectByID(t.Context(), project.ID)
|
||||
}()
|
||||
|
||||
column := &project_model.Column{
|
||||
Title: "Original Column",
|
||||
ProjectID: project.ID,
|
||||
CreatorID: owner.ID,
|
||||
Color: "#000000",
|
||||
}
|
||||
err = project_model.NewColumn(t.Context(), column)
|
||||
assert.NoError(t, err)
|
||||
|
||||
token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// Test updating column title
|
||||
newTitle := "Updated Column"
|
||||
req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/projects/columns/%d", owner.Name, repo.Name, column.ID), &api.EditProjectColumnOption{
|
||||
Title: &newTitle,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var updatedColumn api.ProjectColumn
|
||||
DecodeJSON(t, resp, &updatedColumn)
|
||||
assert.Equal(t, newTitle, updatedColumn.Title)
|
||||
|
||||
// Test updating column color
|
||||
newColor := "#FF0000"
|
||||
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/projects/columns/%d", owner.Name, repo.Name, column.ID), &api.EditProjectColumnOption{
|
||||
Color: &newColor,
|
||||
}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
DecodeJSON(t, resp, &updatedColumn)
|
||||
assert.Equal(t, newColor, updatedColumn.Color)
|
||||
|
||||
// Test updating non-existent column
|
||||
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/projects/columns/99999", owner.Name, repo.Name), &api.EditProjectColumnOption{
|
||||
Title: &newTitle,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIDeleteProjectColumn(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
// Create a test project and column
|
||||
project := &project_model.Project{
|
||||
Title: "Project for Column Deletion",
|
||||
RepoID: repo.ID,
|
||||
Type: project_model.TypeRepository,
|
||||
CreatorID: owner.ID,
|
||||
TemplateType: project_model.TemplateTypeNone,
|
||||
}
|
||||
err := project_model.NewProject(t.Context(), project)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
_ = project_model.DeleteProjectByID(t.Context(), project.ID)
|
||||
}()
|
||||
|
||||
column := &project_model.Column{
|
||||
Title: "Column to Delete",
|
||||
ProjectID: project.ID,
|
||||
CreatorID: owner.ID,
|
||||
}
|
||||
err = project_model.NewColumn(t.Context(), column)
|
||||
assert.NoError(t, err)
|
||||
|
||||
token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// Test deleting the column
|
||||
req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/projects/columns/%d", owner.Name, repo.Name, column.ID).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Test deleting non-existent column (including the one we just deleted)
|
||||
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/projects/columns/%d", owner.Name, repo.Name, column.ID).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIAddIssueToProjectColumn(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
|
||||
|
||||
// Create a test project and column
|
||||
project := &project_model.Project{
|
||||
Title: "Project for Issue Assignment",
|
||||
RepoID: repo.ID,
|
||||
Type: project_model.TypeRepository,
|
||||
CreatorID: owner.ID,
|
||||
TemplateType: project_model.TemplateTypeNone,
|
||||
}
|
||||
err := project_model.NewProject(t.Context(), project)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
_ = project_model.DeleteProjectByID(t.Context(), project.ID)
|
||||
}()
|
||||
|
||||
column1 := &project_model.Column{
|
||||
Title: "Column 1",
|
||||
ProjectID: project.ID,
|
||||
CreatorID: owner.ID,
|
||||
}
|
||||
err = project_model.NewColumn(t.Context(), column1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
column2 := &project_model.Column{
|
||||
Title: "Column 2",
|
||||
ProjectID: project.ID,
|
||||
CreatorID: owner.ID,
|
||||
}
|
||||
err = project_model.NewColumn(t.Context(), column2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// Test adding issue to column
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/projects/columns/%d/issues", owner.Name, repo.Name, column1.ID), &api.AddIssueToProjectColumnOption{
|
||||
IssueID: issue.ID,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// Verify issue is in the column
|
||||
projectIssue := unittest.AssertExistsAndLoadBean(t, &project_model.ProjectIssue{
|
||||
ProjectID: project.ID,
|
||||
IssueID: issue.ID,
|
||||
})
|
||||
assert.Equal(t, column1.ID, projectIssue.ProjectColumnID)
|
||||
|
||||
// Test moving issue to another column
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/projects/columns/%d/issues", owner.Name, repo.Name, column2.ID), &api.AddIssueToProjectColumnOption{
|
||||
IssueID: issue.ID,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// Verify issue moved to new column
|
||||
projectIssue = unittest.AssertExistsAndLoadBean(t, &project_model.ProjectIssue{
|
||||
ProjectID: project.ID,
|
||||
IssueID: issue.ID,
|
||||
})
|
||||
assert.Equal(t, column2.ID, projectIssue.ProjectColumnID)
|
||||
|
||||
// Test adding same issue to same column (should be idempotent)
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/projects/columns/%d/issues", owner.Name, repo.Name, column2.ID), &api.AddIssueToProjectColumnOption{
|
||||
IssueID: issue.ID,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// Test adding non-existent issue
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/projects/columns/%d/issues", owner.Name, repo.Name, column1.ID), &api.AddIssueToProjectColumnOption{
|
||||
IssueID: 99999,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
|
||||
// Test adding to non-existent column
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/projects/columns/99999/issues", owner.Name, repo.Name), &api.AddIssueToProjectColumnOption{
|
||||
IssueID: issue.ID,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIProjectPermissions(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
nonCollaborator := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user5"})
|
||||
|
||||
// Create a test project
|
||||
project := &project_model.Project{
|
||||
Title: "Permission Test Project",
|
||||
RepoID: repo.ID,
|
||||
Type: project_model.TypeRepository,
|
||||
CreatorID: owner.ID,
|
||||
TemplateType: project_model.TemplateTypeNone,
|
||||
}
|
||||
err := project_model.NewProject(t.Context(), project)
|
||||
assert.NoError(t, err)
|
||||
defer func() {
|
||||
_ = project_model.DeleteProjectByID(t.Context(), project.ID)
|
||||
}()
|
||||
|
||||
ownerToken := getUserToken(t, owner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
nonCollaboratorToken := getUserToken(t, nonCollaborator.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// Owner should be able to read
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/projects/%d", owner.Name, repo.Name, project.ID).
|
||||
AddTokenAuth(ownerToken)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Owner should be able to update
|
||||
newTitle := "Updated by Owner"
|
||||
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/projects/%d", owner.Name, repo.Name, project.ID), &api.EditProjectOption{
|
||||
Title: &newTitle,
|
||||
}).AddTokenAuth(ownerToken)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Non-collaborator should not be able to update
|
||||
anotherTitle := "Updated by Non-collaborator"
|
||||
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/projects/%d", owner.Name, repo.Name, project.ID), &api.EditProjectOption{
|
||||
Title: &anotherTitle,
|
||||
}).AddTokenAuth(nonCollaboratorToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
// Non-collaborator should not be able to delete
|
||||
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/projects/%d", owner.Name, repo.Name, project.ID).
|
||||
AddTokenAuth(nonCollaboratorToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user