0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-02-23 15:05:35 +01:00

Add tests for project workflow execution

This commit is contained in:
Lunny Xiao 2025-10-26 22:51:35 -07:00
parent fdd3bf04cb
commit 418a7bb07e
No known key found for this signature in database
GPG Key ID: C3B7C91B632F738A
7 changed files with 883 additions and 41 deletions

View File

@ -685,6 +685,7 @@ func MoveIssues(ctx *context.Context) {
form := &movedIssuesForm{}
if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
ctx.ServerError("DecodeMovedIssuesForm", err)
return
}
issueIDs := make([]int64, 0, len(form.Issues))

View File

@ -8,7 +8,6 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
user_model "code.gitea.io/gitea/models/user"
notify_service "code.gitea.io/gitea/services/notify"
)
@ -47,21 +46,6 @@ func AddLabels(ctx context.Context, issue *issues_model.Issue, doer *user_model.
// RemoveLabel removes a label from issue by given ID.
func RemoveLabel(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, label *issues_model.Label) error {
if err := db.WithTx(ctx, func(ctx context.Context) error {
if err := issue.LoadRepo(ctx); err != nil {
return err
}
perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
if err != nil {
return err
}
if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
if label.OrgID > 0 {
return issues_model.ErrOrgLabelNotExist{}
}
return issues_model.ErrRepoLabelNotExist{}
}
return issues_model.DeleteIssueLabel(ctx, issue, label, doer)
}); err != nil {
return err

View File

@ -98,7 +98,27 @@ func (m *workflowNotifier) IssueChangeStatus(ctx context.Context, doer *user_mod
}
func (*workflowNotifier) IssueChangeProjects(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, newProject *project_model.Project) {
if newProject == nil {
if newProject == nil { // removed from project
if err := issue.LoadProject(ctx); err != nil {
log.Error("LoadProject: %v", err)
return
}
if issue.Project == nil {
return
}
workflows, err := project_model.FindWorkflowsByProjectID(ctx, issue.Project.ID)
if err != nil {
log.Error("IssueChangeStatus: FindWorkflowsByProjectID: %v", err)
return
}
// Find workflows for the ItemOpened event
for _, workflow := range workflows {
if workflow.WorkflowEvent == project_model.WorkflowEventItemRemovedFromProject {
fireIssueWorkflow(ctx, workflow, issue, 0, 0)
}
}
return
}
@ -381,7 +401,6 @@ func executeWorkflowActions(ctx context.Context, workflow *project_model.Workflo
log.Error("ReopenIssue: %v", err)
continue
}
issue.IsClosed = false
}
} else if strings.EqualFold(action.Value, "close") {
if !issue.IsClosed {
@ -389,7 +408,6 @@ func executeWorkflowActions(ctx context.Context, workflow *project_model.Workflo
log.Error("CloseIssue: %v", err)
continue
}
issue.IsClosed = true
}
}
default:

View File

@ -122,17 +122,34 @@ func TestNoLoginViewIssue(t *testing.T) {
MakeRequest(t, req, http.StatusOK)
}
func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content string) string {
type newIssueOptions struct {
Title string
Content string
ProjectID int64
LabelIDs []int64
}
func testNewIssue(t *testing.T, session *TestSession, user, repo string, opts newIssueOptions) string {
req := NewRequest(t, "GET", path.Join(user, repo, "issues", "new"))
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
assert.True(t, exists, "The template has changed")
var labelIDs string
for i, id := range opts.LabelIDs {
labelIDs += strconv.FormatInt(id, 10)
if i < len(opts.LabelIDs)-1 {
labelIDs += ","
}
}
req = NewRequestWithValues(t, "POST", link, map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"title": title,
"content": content,
"_csrf": htmlDoc.GetCSRF(),
"title": opts.Title,
"content": opts.Content,
"project_id": strconv.FormatInt(opts.ProjectID, 10),
"label_ids": labelIDs,
})
resp = session.MakeRequest(t, req, http.StatusOK)
@ -142,9 +159,9 @@ func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content
htmlDoc = NewHTMLParser(t, resp.Body)
val := htmlDoc.doc.Find("#issue-title-display").Text()
assert.Contains(t, val, title)
assert.Contains(t, val, opts.Title)
val = htmlDoc.doc.Find(".comment .render-content p").First().Text()
assert.Equal(t, content, val)
assert.Equal(t, opts.Content, val)
return issueURL
}
@ -210,13 +227,19 @@ func testIssueChangeMilestone(t *testing.T, session *TestSession, repoLink strin
func TestNewIssue(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
testNewIssue(t, session, "user2", "repo1", "Title", "Description")
testNewIssue(t, session, "user2", "repo1", newIssueOptions{
Title: "Title",
Content: "Description",
})
}
func TestEditIssue(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
issueURL := testNewIssue(t, session, "user2", "repo1", newIssueOptions{
Title: "Title",
Content: "Description",
})
req := NewRequestWithValues(t, "POST", issueURL+"/content", map[string]string{
"_csrf": GetUserCSRFToken(t, session),
@ -244,7 +267,10 @@ func TestEditIssue(t *testing.T) {
func TestIssueCommentClose(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
issueURL := testNewIssue(t, session, "user2", "repo1", newIssueOptions{
Title: "Title",
Content: "Description",
})
testIssueAddComment(t, session, issueURL, "Test comment 1", "")
testIssueAddComment(t, session, issueURL, "Test comment 2", "")
testIssueAddComment(t, session, issueURL, "Test comment 3", "close")
@ -260,7 +286,10 @@ func TestIssueCommentClose(t *testing.T) {
func TestIssueCommentDelete(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
issueURL := testNewIssue(t, session, "user2", "repo1", newIssueOptions{
Title: "Title",
Content: "Description",
})
comment1 := "Test comment 1"
commentID := testIssueAddComment(t, session, issueURL, comment1, "")
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID})
@ -281,7 +310,10 @@ func TestIssueCommentDelete(t *testing.T) {
func TestIssueCommentUpdate(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
issueURL := testNewIssue(t, session, "user2", "repo1", newIssueOptions{
Title: "Title",
Content: "Description",
})
comment1 := "Test comment 1"
commentID := testIssueAddComment(t, session, issueURL, comment1, "")
@ -310,7 +342,10 @@ func TestIssueCommentUpdate(t *testing.T) {
func TestIssueCommentUpdateSimultaneously(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
issueURL := testNewIssue(t, session, "user2", "repo1", newIssueOptions{
Title: "Title",
Content: "Description",
})
comment1 := "Test comment 1"
commentID := testIssueAddComment(t, session, issueURL, comment1, "")
@ -348,7 +383,10 @@ func TestIssueCommentUpdateSimultaneously(t *testing.T) {
func TestIssueReaction(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description")
issueURL := testNewIssue(t, session, "user2", "repo1", newIssueOptions{
Title: "Title",
Content: "Description",
})
req := NewRequest(t, "GET", issueURL)
resp := session.MakeRequest(t, req, http.StatusOK)
@ -448,7 +486,10 @@ func TestIssueCrossReference(t *testing.T) {
func testIssueWithBean(t *testing.T, user string, repoID int64, title, content string) (string, *issues_model.Issue) {
session := loginUser(t, user)
issueURL := testNewIssue(t, session, user, fmt.Sprintf("repo%d", repoID), title, content)
issueURL := testNewIssue(t, session, user, fmt.Sprintf("repo%d", repoID), newIssueOptions{
Title: title,
Content: content,
})
indexStr := issueURL[strings.LastIndexByte(issueURL, '/')+1:]
index, err := strconv.Atoi(indexStr)
assert.NoError(t, err, "Invalid issue href: %s", issueURL)

View File

@ -0,0 +1,774 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"testing"
"code.gitea.io/gitea/models/db"
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"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func testCreateProjectWorkflow(t *testing.T, session *TestSession, userName, repoName string, projectID int64, event string, workflowData map[string]any) {
req := NewRequestWithJSON(t, "POST",
fmt.Sprintf("/%s/%s/projects/%d/workflows/%s?_csrf=%s",
userName, repoName, projectID, event, GetUserCSRFToken(t, session)),
workflowData)
resp := session.MakeRequest(t, req, http.StatusOK)
var result map[string]any
err := json.Unmarshal(resp.Body.Bytes(), &result)
assert.NoError(t, err)
assert.True(t, result["success"].(bool))
}
func testNewIssueReturnIssue(t *testing.T, session *TestSession, repo *repo_model.Repository, opts newIssueOptions) int64 {
testNewIssue(t, session, repo.OwnerName, repo.Name, opts)
// Get the created issue from database to verify
issues, err := issues_model.Issues(t.Context(), &issues_model.IssuesOptions{
RepoIDs: []int64{repo.ID},
SortType: "newest",
Paginator: &db.ListOptions{
PageSize: 1,
},
})
assert.NoError(t, err)
assert.NotEmpty(t, issues)
return issues[0].ID
}
// testAddIssueToProject adds the issue to the project via web form if projectID == 0, it removes the issue from the project
func testAddIssueToProject(t *testing.T, session *TestSession, userName, repoName string, projectID, issueID int64) {
addToProjectReq := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/issues/projects?_csrf=%s",
userName, repoName, GetUserCSRFToken(t, session)),
map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"id": strconv.FormatInt(projectID, 10),
"issue_ids": strconv.FormatInt(issueID, 10),
})
session.MakeRequest(t, addToProjectReq, http.StatusOK)
}
// TestProjectWorkflowExecutionItemOpened tests workflow execution when an issue is added to project
func TestProjectWorkflowExecutionItemOpened(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
// Create project and columns
project := &project_model.Project{
Title: "Test Workflow Execution",
RepoID: repo.ID,
Type: project_model.TypeRepository,
TemplateType: project_model.TemplateTypeNone,
}
assert.NoError(t, project_model.NewProject(t.Context(), project))
columnToDo := &project_model.Column{
Title: "To Do",
ProjectID: project.ID,
}
assert.NoError(t, project_model.NewColumn(t.Context(), columnToDo))
// Create label
label := &issues_model.Label{
RepoID: repo.ID,
Name: "bug",
Color: "ee0701",
}
assert.NoError(t, issues_model.NewLabel(t.Context(), label))
session := loginUser(t, user.Name)
// Create workflow via HTTP: when item is opened, move to "To Do" and add "bug" label
testCreateProjectWorkflow(t, session, user.Name, repo.Name, project.ID, "item_opened", map[string]any{
"event_id": string(project_model.WorkflowEventItemOpened),
"filters": map[string]any{
string(project_model.WorkflowFilterTypeIssueType): "issue",
},
"actions": map[string]any{
string(project_model.WorkflowActionTypeColumn): strconv.FormatInt(columnToDo.ID, 10),
string(project_model.WorkflowActionTypeAddLabels): []string{strconv.FormatInt(label.ID, 10)},
},
})
issueID := testNewIssueReturnIssue(t, session, repo, newIssueOptions{
Title: "Test Issue for Workflow",
Content: "This should trigger item_opened workflow",
ProjectID: project.ID,
})
// Verify workflow executed: issue moved to "To Do" and has "bug" label
issue, err := issues_model.GetIssueByID(t.Context(), issueID)
assert.NoError(t, err)
projectIssue := &project_model.ProjectIssue{}
has, err := db.GetEngine(t.Context()).Where("issue_id=?", issue.ID).Get(projectIssue)
assert.NoError(t, err)
assert.True(t, has)
assert.Equal(t, columnToDo.ID, projectIssue.ProjectColumnID)
err = issue.LoadLabels(t.Context())
assert.NoError(t, err)
assert.Len(t, issue.Labels, 1)
assert.Equal(t, label.ID, issue.Labels[0].ID)
}
func TestProjectWorkflowExecutionItemAddedToProject(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
// Create project and columns
project := &project_model.Project{
Title: "Test Workflow Execution",
RepoID: repo.ID,
Type: project_model.TypeRepository,
TemplateType: project_model.TemplateTypeNone,
}
assert.NoError(t, project_model.NewProject(t.Context(), project))
columnToDo := &project_model.Column{
Title: "To Do",
ProjectID: project.ID,
}
assert.NoError(t, project_model.NewColumn(t.Context(), columnToDo))
// Create label
label := &issues_model.Label{
RepoID: repo.ID,
Name: "bug",
Color: "ee0701",
}
assert.NoError(t, issues_model.NewLabel(t.Context(), label))
session := loginUser(t, user.Name)
// Create workflow via HTTP: when item added to project, move to "To Do" and add "bug" label
testCreateProjectWorkflow(t, session, user.Name, repo.Name, project.ID, "item_added_to_project", map[string]any{
"event_id": string(project_model.WorkflowEventItemAddedToProject),
"filters": map[string]any{
string(project_model.WorkflowFilterTypeIssueType): "issue",
},
"actions": map[string]any{
string(project_model.WorkflowActionTypeColumn): strconv.FormatInt(columnToDo.ID, 10),
string(project_model.WorkflowActionTypeAddLabels): []string{strconv.FormatInt(label.ID, 10)},
},
})
issueID := testNewIssueReturnIssue(t, session, repo, newIssueOptions{
Title: "Test Issue for Workflow",
Content: "This should trigger workflow when added to project",
})
// Add issue to project via Web form - this triggers the workflow
testAddIssueToProject(t, session, user.Name, repo.Name, project.ID, issueID)
// Verify workflow executed: issue moved to "To Do" and has "bug" label
issue, err := issues_model.GetIssueByID(t.Context(), issueID)
assert.NoError(t, err)
projectIssue := &project_model.ProjectIssue{}
has, err := db.GetEngine(t.Context()).Where("issue_id=?", issue.ID).Get(projectIssue)
assert.NoError(t, err)
assert.True(t, has)
assert.Equal(t, columnToDo.ID, projectIssue.ProjectColumnID)
err = issue.LoadLabels(t.Context())
assert.NoError(t, err)
assert.Len(t, issue.Labels, 1)
assert.Equal(t, label.ID, issue.Labels[0].ID)
}
func TestProjectWorkflowExecutionItemRemovedFromProject(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
// Create project and columns
project := &project_model.Project{
Title: "Test Workflow Execution",
RepoID: repo.ID,
Type: project_model.TypeRepository,
TemplateType: project_model.TemplateTypeNone,
}
assert.NoError(t, project_model.NewProject(t.Context(), project))
columnToDo := &project_model.Column{
Title: "To Do",
ProjectID: project.ID,
}
assert.NoError(t, project_model.NewColumn(t.Context(), columnToDo))
// Create label
label := &issues_model.Label{
RepoID: repo.ID,
Name: "no-project",
Color: "ee0701",
}
assert.NoError(t, issues_model.NewLabel(t.Context(), label))
session := loginUser(t, user.Name)
// Create workflow via HTTP: when item added to project, move to "To Do" and add "bug" label
testCreateProjectWorkflow(t, session, user.Name, repo.Name, project.ID, "item_removed_from_project", map[string]any{
"event_id": string(project_model.WorkflowEventItemRemovedFromProject),
"filters": map[string]any{
string(project_model.WorkflowFilterTypeIssueType): "issue",
},
"actions": map[string]any{
string(project_model.WorkflowActionTypeAddLabels): []string{strconv.FormatInt(label.ID, 10)},
},
})
issueID := testNewIssueReturnIssue(t, session, repo, newIssueOptions{
Title: "Test Issue for Workflow",
Content: "This should trigger workflow when removed from project",
ProjectID: project.ID,
})
// remove issue from the project to trigger the workflow
testAddIssueToProject(t, session, user.Name, repo.Name, 0, issueID)
issue, err := issues_model.GetIssueByID(t.Context(), issueID)
assert.NoError(t, err)
assert.NoError(t, issue.LoadProject(t.Context()))
assert.Nil(t, issue.Project)
err = issue.LoadLabels(t.Context())
assert.NoError(t, err)
assert.Len(t, issue.Labels, 1)
assert.Equal(t, label.ID, issue.Labels[0].ID)
}
// TestProjectWorkflowExecutionItemClosed tests workflow when issue is closed
func TestProjectWorkflowExecutionItemClosed(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
project := &project_model.Project{
Title: "Test Close Workflow",
RepoID: repo.ID,
Type: project_model.TypeRepository,
TemplateType: project_model.TemplateTypeNone,
}
err := project_model.NewProject(t.Context(), project)
assert.NoError(t, err)
columnDone := &project_model.Column{
Title: "Done",
ProjectID: project.ID,
}
err = project_model.NewColumn(t.Context(), columnDone)
assert.NoError(t, err)
labelCompleted := &issues_model.Label{
RepoID: repo.ID,
Name: "completed",
Color: "00ff00",
}
err = issues_model.NewLabel(t.Context(), labelCompleted)
assert.NoError(t, err)
session := loginUser(t, user.Name)
// Create workflow: when closed, move to "Done" and add "completed" label
testCreateProjectWorkflow(t, session, user.Name, repo.Name, project.ID, "item_closed", map[string]any{
"event_id": string(project_model.WorkflowEventItemClosed),
"filters": map[string]any{
string(project_model.WorkflowFilterTypeIssueType): "issue",
},
"actions": map[string]any{
string(project_model.WorkflowActionTypeColumn): strconv.FormatInt(columnDone.ID, 10),
string(project_model.WorkflowActionTypeAddLabels): []string{strconv.FormatInt(labelCompleted.ID, 10)},
},
})
issueID := testNewIssueReturnIssue(t, session, repo, newIssueOptions{
Title: "Test Issue for Workflow",
Content: "This should trigger workflow when item is closed",
ProjectID: project.ID,
})
issue, err := issues_model.GetIssueByID(t.Context(), issueID)
assert.NoError(t, err)
assert.False(t, issue.IsClosed)
assert.NoError(t, issue.LoadRepo(t.Context()))
// Close issue via API
testIssueAddComment(t, session, issue.Link(), "Test comment 3", "close")
// Verify workflow executed
issue, err = issues_model.GetIssueByID(t.Context(), issueID)
assert.NoError(t, err)
assert.True(t, issue.IsClosed)
projectIssue := &project_model.ProjectIssue{}
has, err := db.GetEngine(t.Context()).Where("issue_id=?", issue.ID).Get(projectIssue)
assert.NoError(t, err)
assert.True(t, has)
assert.Equal(t, columnDone.ID, projectIssue.ProjectColumnID)
err = issue.LoadLabels(t.Context())
assert.NoError(t, err)
hasLabel := false
for _, l := range issue.Labels {
if l.ID == labelCompleted.ID {
hasLabel = true
break
}
}
assert.True(t, hasLabel)
}
func TestProjectWorkflowExecutionItemReopened(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
project := &project_model.Project{
Title: "Test Close Workflow",
RepoID: repo.ID,
Type: project_model.TypeRepository,
TemplateType: project_model.TemplateTypeNone,
}
err := project_model.NewProject(t.Context(), project)
assert.NoError(t, err)
columnDone := &project_model.Column{
Title: "Done",
ProjectID: project.ID,
}
err = project_model.NewColumn(t.Context(), columnDone)
assert.NoError(t, err)
labelCompleted := &issues_model.Label{
RepoID: repo.ID,
Name: "completed",
Color: "00ff00",
}
err = issues_model.NewLabel(t.Context(), labelCompleted)
assert.NoError(t, err)
session := loginUser(t, user.Name)
testCreateProjectWorkflow(t, session, user.Name, repo.Name, project.ID, "item_reopened",
map[string]any{
"event_id": string(project_model.WorkflowEventItemReopened),
"filters": map[string]any{
string(project_model.WorkflowFilterTypeIssueType): "issue",
string(project_model.WorkflowFilterTypeLabels): strconv.FormatInt(labelCompleted.ID, 10),
},
"actions": map[string]any{
string(project_model.WorkflowActionTypeColumn): strconv.FormatInt(columnDone.ID, 10),
},
})
issueID := testNewIssueReturnIssue(t, session, repo, newIssueOptions{
Title: "Test Issue for Workflow",
Content: "This should trigger workflow when item is reopened",
ProjectID: project.ID,
LabelIDs: []int64{labelCompleted.ID},
})
issue, err := issues_model.GetIssueByID(t.Context(), issueID)
assert.NoError(t, err)
assert.False(t, issue.IsClosed)
assert.NoError(t, issue.LoadRepo(t.Context()))
// Reopen issue
testIssueAddComment(t, session, issue.Link(), "Test comment 3", "close")
testIssueAddComment(t, session, issue.Link(), "Test comment 3", "reopen")
// Reload and Verify workflow executed
issue, err = issues_model.GetIssueByID(t.Context(), issueID)
assert.NoError(t, err)
assert.False(t, issue.IsClosed)
projectIssue := &project_model.ProjectIssue{}
has, err := db.GetEngine(t.Context()).Where("issue_id=?", issue.ID).Get(projectIssue)
assert.NoError(t, err)
assert.True(t, has)
assert.Equal(t, columnDone.ID, projectIssue.ProjectColumnID)
err = issue.LoadLabels(t.Context())
assert.NoError(t, err)
hasLabel := false
for _, l := range issue.Labels {
if l.ID == labelCompleted.ID {
hasLabel = true
break
}
}
assert.True(t, hasLabel)
}
// TestProjectWorkflowExecutionColumnChanged tests workflow when moving between columns
func TestProjectWorkflowExecutionColumnChanged(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
project := &project_model.Project{
Title: "Test Column Change",
RepoID: repo.ID,
Type: project_model.TypeRepository,
TemplateType: project_model.TemplateTypeNone,
}
err := project_model.NewProject(t.Context(), project)
assert.NoError(t, err)
columnToDo := &project_model.Column{Title: "To Do", ProjectID: project.ID}
err = project_model.NewColumn(t.Context(), columnToDo)
assert.NoError(t, err)
columnDone := &project_model.Column{Title: "Done", ProjectID: project.ID}
err = project_model.NewColumn(t.Context(), columnDone)
assert.NoError(t, err)
labelWIP := &issues_model.Label{RepoID: repo.ID, Name: "wip", Color: "fbca04"}
err = issues_model.NewLabel(t.Context(), labelWIP)
assert.NoError(t, err)
session := loginUser(t, user.Name)
// Create workflow: when moved to "Done", remove "wip" and close
testCreateProjectWorkflow(t, session, user.Name, repo.Name, project.ID, "item_column_changed", map[string]any{
"event_id": string(project_model.WorkflowEventItemColumnChanged),
"filters": map[string]any{
string(project_model.WorkflowFilterTypeTargetColumn): strconv.FormatInt(columnDone.ID, 10),
},
"actions": map[string]any{
string(project_model.WorkflowActionTypeRemoveLabels): []string{strconv.FormatInt(labelWIP.ID, 10)},
string(project_model.WorkflowActionTypeIssueState): "close",
},
})
// Create issue with "wip" label
issueID := testNewIssueReturnIssue(t, session, repo, newIssueOptions{
Title: "Test Column Change",
Content: "Will move columns",
ProjectID: project.ID,
LabelIDs: []int64{labelWIP.ID},
})
// Move to "To Do" first
moveReq := NewRequestWithJSON(t, "POST",
fmt.Sprintf("/%s/%s/projects/%d/%d/move?_csrf=%s", user.Name, repo.Name, project.ID, columnToDo.ID, GetUserCSRFToken(t, session)),
map[string]any{
"issues": []map[string]any{
{
"issueID": issueID,
"sorting": 0,
},
},
})
session.MakeRequest(t, moveReq, http.StatusOK)
// Move to "Done" - triggers workflow
moveReq = NewRequestWithJSON(t, "POST",
fmt.Sprintf("/%s/%s/projects/%d/%d/move?_csrf=%s", user.Name, repo.Name, project.ID, columnDone.ID, GetUserCSRFToken(t, session)),
map[string]any{
"issues": []map[string]any{
{
"issueID": issueID,
"sorting": 0,
},
},
})
session.MakeRequest(t, moveReq, http.StatusOK)
// Verify workflow executed
issue, err := issues_model.GetIssueByID(t.Context(), issueID)
assert.NoError(t, err)
assert.True(t, issue.IsClosed, "Issue should be closed")
err = issue.LoadLabels(t.Context())
assert.NoError(t, err)
hasWIP := false
for _, l := range issue.Labels {
if l.ID == labelWIP.ID {
hasWIP = true
break
}
}
assert.False(t, hasWIP, "WIP label should be removed")
}
func TestProjectWorkflowExecutionCodeChangesRequested(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// Use existing PR #3 from fixtures (issue_id: 3, pull_request id: 2)
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
assert.NoError(t, pr.LoadIssue(t.Context()))
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
repo := pr.BaseRepo
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
project := &project_model.Project{
Title: "Test Code Changes Requested",
RepoID: repo.ID,
Type: project_model.TypeRepository,
TemplateType: project_model.TemplateTypeNone,
}
err := project_model.NewProject(t.Context(), project)
assert.NoError(t, err)
columnInProgress := &project_model.Column{Title: "In Progress", ProjectID: project.ID}
err = project_model.NewColumn(t.Context(), columnInProgress)
assert.NoError(t, err)
labelNeedChange := &issues_model.Label{RepoID: repo.ID, Name: "needs-changes", Color: "fbca04"}
err = issues_model.NewLabel(t.Context(), labelNeedChange)
assert.NoError(t, err)
session := loginUser(t, user.Name)
// Create workflow: when code changes requested, add "needs-changes" label
testCreateProjectWorkflow(t, session, user.Name, repo.Name, project.ID, "code_changes_requested", map[string]any{
"event_id": string(project_model.WorkflowEventCodeChangesRequested),
"filters": map[string]any{},
"actions": map[string]any{
string(project_model.WorkflowActionTypeAddLabels): []string{strconv.FormatInt(labelNeedChange.ID, 10)},
},
})
// Add PR to project
testAddIssueToProject(t, session, user.Name, repo.Name, project.ID, pr.Issue.ID)
// User 2 submits a "REQUEST_CHANGES" review
user2Session := loginUser(t, "user2")
prURL := fmt.Sprintf("/%s/%s/pulls/%d", user.Name, repo.Name, pr.Issue.Index)
req := NewRequest(t, "GET", prURL+"/files")
resp := user2Session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
gitRepo, err := gitrepo.OpenRepository(t.Context(), pr.BaseRepo)
assert.NoError(t, err)
defer gitRepo.Close()
commitID, err := gitRepo.GetRefCommitID(pr.GetGitHeadRefName())
assert.NoError(t, err)
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), user.Name, repo.Name, strconv.FormatInt(int64(pr.Issue.Index), 10), commitID, "reject", http.StatusOK)
// Verify workflow executed: PR should have "needs-changes" label
issue, err := issues_model.GetIssueByID(t.Context(), pr.Issue.ID)
assert.NoError(t, err)
err = issue.LoadLabels(t.Context())
assert.NoError(t, err)
hasNeedChangeLabel := false
for _, l := range issue.Labels {
if l.ID == labelNeedChange.ID {
hasNeedChangeLabel = true
break
}
}
assert.True(t, hasNeedChangeLabel, "needs-changes label should be added")
}
func TestProjectWorkflowExecutionCodeReviewApproved(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// Use existing PR #3 from fixtures (issue_id: 3, pull_request id: 2)
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
assert.NoError(t, pr.LoadIssue(t.Context()))
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
repo := pr.BaseRepo
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
project := &project_model.Project{
Title: "Test Code Review Approved",
RepoID: repo.ID,
Type: project_model.TypeRepository,
TemplateType: project_model.TemplateTypeNone,
}
err := project_model.NewProject(t.Context(), project)
assert.NoError(t, err)
columnReadyToMerge := &project_model.Column{Title: "Ready to Merge", ProjectID: project.ID}
err = project_model.NewColumn(t.Context(), columnReadyToMerge)
assert.NoError(t, err)
labelApproved := &issues_model.Label{RepoID: repo.ID, Name: "approved", Color: "00ff00"}
err = issues_model.NewLabel(t.Context(), labelApproved)
assert.NoError(t, err)
session := loginUser(t, user.Name)
// Create workflow: when code review approved, move to "Ready to Merge" and add "approved" label
testCreateProjectWorkflow(t, session, user.Name, repo.Name, project.ID, "code_review_approved", map[string]any{
"event_id": string(project_model.WorkflowEventCodeReviewApproved),
"filters": map[string]any{},
"actions": map[string]any{
string(project_model.WorkflowActionTypeColumn): strconv.FormatInt(columnReadyToMerge.ID, 10),
string(project_model.WorkflowActionTypeAddLabels): []string{strconv.FormatInt(labelApproved.ID, 10)},
},
})
// Add PR to project
testAddIssueToProject(t, session, user.Name, repo.Name, project.ID, pr.Issue.ID)
// User 2 submits an "APPROVE" review
user2Session := loginUser(t, "user2")
prURL := fmt.Sprintf("/%s/%s/pulls/%d", user.Name, repo.Name, pr.Issue.Index)
req := NewRequest(t, "GET", prURL+"/files")
resp := user2Session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
gitRepo, err := gitrepo.OpenRepository(t.Context(), pr.BaseRepo)
assert.NoError(t, err)
defer gitRepo.Close()
commitID, err := gitRepo.GetRefCommitID(pr.GetGitHeadRefName())
assert.NoError(t, err)
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), user.Name, repo.Name, fmt.Sprintf("%d", pr.Issue.Index), commitID, "approve", http.StatusOK)
// Verify workflow executed: PR should be in "Ready to Merge" column and have "approved" label
issue, err := issues_model.GetIssueByID(t.Context(), pr.Issue.ID)
assert.NoError(t, err)
err = issue.LoadLabels(t.Context())
assert.NoError(t, err)
hasApprovedLabel := false
for _, l := range issue.Labels {
if l.ID == labelApproved.ID {
hasApprovedLabel = true
break
}
}
assert.True(t, hasApprovedLabel, "approved label should be added")
// Check column
projectIssue := &project_model.ProjectIssue{}
has, err := db.GetEngine(t.Context()).Where("issue_id=?", issue.ID).Get(projectIssue)
assert.NoError(t, err)
assert.True(t, has)
assert.Equal(t, columnReadyToMerge.ID, projectIssue.ProjectColumnID)
}
func TestProjectWorkflowExecutionPullRequestMerged(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
// Fork repo1 and create a PR that can be merged
session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited for merge test)\n")
// Get the base repo
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
// Create project in base repo
project := &project_model.Project{
Title: "Test PR Merged",
RepoID: repo.ID,
Type: project_model.TypeRepository,
TemplateType: project_model.TemplateTypeNone,
}
err := project_model.NewProject(t.Context(), project)
assert.NoError(t, err)
columnDone := &project_model.Column{Title: "Done", ProjectID: project.ID}
err = project_model.NewColumn(t.Context(), columnDone)
assert.NoError(t, err)
labelMerged := &issues_model.Label{RepoID: repo.ID, Name: "merged", Color: "6f42c1"}
err = issues_model.NewLabel(t.Context(), labelMerged)
assert.NoError(t, err)
// Login as user2 (repo owner) to create workflow
user2Session := loginUser(t, "user2")
// Create workflow: when PR merged, move to "Done" and add "merged" label
testCreateProjectWorkflow(t, user2Session, "user2", "repo1", project.ID, "pull_request_merged", map[string]any{
"event_id": string(project_model.WorkflowEventPullRequestMerged),
"filters": map[string]any{},
"actions": map[string]any{
string(project_model.WorkflowActionTypeColumn): strconv.FormatInt(columnDone.ID, 10),
string(project_model.WorkflowActionTypeAddLabels): []string{strconv.FormatInt(labelMerged.ID, 10)},
},
})
// Create PR from user1's fork to user2's repo
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "Test PR for Merge Workflow")
// Get PR details from redirect URL
elem := strings.Split(test.RedirectURL(resp), "/")
assert.Equal(t, "pulls", elem[3])
prNum := elem[4]
// Load the PR
prNumInt, err := strconv.ParseInt(prNum, 10, 64)
assert.NoError(t, err)
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, Index: prNumInt})
assert.NoError(t, pr.LoadIssue(t.Context()))
// Add PR to project (as user2, the repo owner)
testAddIssueToProject(t, user2Session, "user2", "repo1", project.ID, pr.Issue.ID)
// Merge the PR (as user2, who has permission)
prURL := fmt.Sprintf("/user2/repo1/pulls/%s", prNum)
req := NewRequest(t, "GET", prURL)
resp = user2Session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
req = NewRequestWithValues(t, "POST", path.Join(prURL, "merge"), map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"do": string(repo_model.MergeStyleMerge),
})
user2Session.MakeRequest(t, req, http.StatusOK)
// Verify workflow executed: PR should be in "Done" column and have "merged" label
issue, err := issues_model.GetIssueByID(t.Context(), pr.Issue.ID)
assert.NoError(t, err)
err = issue.LoadLabels(t.Context())
assert.NoError(t, err)
hasMergedLabel := false
for _, l := range issue.Labels {
if l.ID == labelMerged.ID {
hasMergedLabel = true
break
}
}
assert.True(t, hasMergedLabel, "merged label should be added")
// Check column
projectIssue := &project_model.ProjectIssue{}
has, err := db.GetEngine(t.Context()).Where("issue_id=?", issue.ID).Get(projectIssue)
assert.NoError(t, err)
assert.True(t, has)
assert.Equal(t, columnDone.ID, projectIssue.ProjectColumnID)
// Verify PR is merged
pr, err = issues_model.GetPullRequestByID(t.Context(), pr.ID)
assert.NoError(t, err)
assert.True(t, pr.HasMerged, "PR should be merged")
})
}

View File

@ -39,9 +39,18 @@ func TestRepoActivity(t *testing.T) {
testPullCreate(t, session, "user1", "repo1", false, "master", "feat/much_better_readme", "This is a pull title")
// Create issues (3 new issues)
testNewIssue(t, session, "user2", "repo1", "Issue 1", "Description 1")
testNewIssue(t, session, "user2", "repo1", "Issue 2", "Description 2")
testNewIssue(t, session, "user2", "repo1", "Issue 3", "Description 3")
testNewIssue(t, session, "user2", "repo1", newIssueOptions{
Title: "Issue 1",
Content: "Description 1",
})
testNewIssue(t, session, "user2", "repo1", newIssueOptions{
Title: "Issue 2",
Content: "Description 2",
})
testNewIssue(t, session, "user2", "repo1", newIssueOptions{
Title: "Issue 3",
Content: "Description 3",
})
// Create releases (1 new release)
createNewRelease(t, session, "/user2/repo1", "v1.0.0", "v1.0.0", false, false)

View File

@ -255,7 +255,10 @@ func Test_WebhookIssueComment(t *testing.T) {
t.Run("create comment", func(t *testing.T) {
// 2. trigger the webhook
issueURL := testNewIssue(t, session, "user2", "repo1", "Title2", "Description2")
issueURL := testNewIssue(t, session, "user2", "repo1", newIssueOptions{
Title: "Title2",
Content: "Description2",
})
testIssueAddComment(t, session, issueURL, "issue title2 comment1", "")
// 3. validate the webhook is triggered
@ -274,7 +277,10 @@ func Test_WebhookIssueComment(t *testing.T) {
triggeredEvent = ""
// 2. trigger the webhook
issueURL := testNewIssue(t, session, "user2", "repo1", "Title3", "Description3")
issueURL := testNewIssue(t, session, "user2", "repo1", newIssueOptions{
Title: "Title3",
Content: "Description3",
})
commentID := testIssueAddComment(t, session, issueURL, "issue title3 comment1", "")
modifiedContent := "issue title2 comment1 - modified"
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{
@ -300,7 +306,10 @@ func Test_WebhookIssueComment(t *testing.T) {
commentContent := "issue title3 comment1"
// 2. trigger the webhook
issueURL := testNewIssue(t, session, "user2", "repo1", "Title3", "Description3")
issueURL := testNewIssue(t, session, "user2", "repo1", newIssueOptions{
Title: "Title3",
Content: "Description3",
})
commentID := testIssueAddComment(t, session, issueURL, commentContent, "")
payloads = make([]api.IssueCommentPayload, 0, 2)
@ -511,7 +520,10 @@ func Test_WebhookIssue(t *testing.T) {
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issues")
// 2. trigger the webhook
testNewIssue(t, session, "user2", "repo1", "Title1", "Description1")
testNewIssue(t, session, "user2", "repo1", newIssueOptions{
Title: "Title1",
Content: "Description1",
})
// 3. validate the webhook is triggered
assert.Equal(t, "issues", triggeredEvent)
@ -543,7 +555,10 @@ func Test_WebhookIssueDelete(t *testing.T) {
// 1. create a new webhook with special webhook for repo1
session := loginUser(t, "user2")
testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issues")
issueURL := testNewIssue(t, session, "user2", "repo1", "Title1", "Description1")
issueURL := testNewIssue(t, session, "user2", "repo1", newIssueOptions{
Title: "Title1",
Content: "Description1",
})
// 2. trigger the webhook
testIssueDelete(t, session, issueURL)