Files
gitea/services/projects/workflow_test.go
T
silverwind 8abda62312 test: consolidate Go tests (#39962)
Consolidate all Go tests. Coverage is identical to before. Go tests are
around 5% faster while being 7% smaller. No non-test code is touched.
2026-10-24 13:48:19 +00:00

287 lines
16 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package project
import (
"context"
"testing"
audit_model "gitea.dev/models/audit"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
project_model "gitea.dev/models/project"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/test"
"gitea.dev/modules/util"
issue_service "gitea.dev/services/issue"
notify_service "gitea.dev/services/notify"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMatchWorkflowFilters(t *testing.T) {
issue := &issues_model.Issue{Labels: []*issues_model.Label{{ID: 1}}}
pull := &issues_model.Issue{IsPull: true}
moved := workflowTrigger{sourceColumnID: 1, targetColumnID: 2}
added, columnChanged := project_model.WorkflowEventItemAddedToProject, project_model.WorkflowEventItemColumnChanged
tests := []struct {
name string
event project_model.WorkflowEvent
filters project_model.WorkflowFilters
issue *issues_model.Issue
trigger workflowTrigger
want bool
}{
{"no filters", added, project_model.WorkflowFilters{}, issue, workflowTrigger{}, true},
{"issue type", added, project_model.WorkflowFilters{IssueType: "issue"}, issue, workflowTrigger{}, true},
{"other issue type", added, project_model.WorkflowFilters{IssueType: "issue"}, pull, workflowTrigger{}, false},
{"label", added, project_model.WorkflowFilters{LabelIDs: []int64{1}}, issue, workflowTrigger{}, true},
{"missing label", added, project_model.WorkflowFilters{LabelIDs: []int64{1, 2}}, issue, workflowTrigger{}, false},
{"columns", columnChanged, project_model.WorkflowFilters{SourceColumnID: 1, TargetColumnID: 2}, issue, moved, true},
{"other target", columnChanged, project_model.WorkflowFilters{TargetColumnID: 3}, issue, moved, false},
{"no column supplied", columnChanged, project_model.WorkflowFilters{SourceColumnID: 1}, issue, workflowTrigger{}, false},
{"unsupported by event", added, project_model.WorkflowFilters{TargetColumnID: 2}, issue, moved, false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
wf := &project_model.Workflow{WorkflowEvent: test.event, WorkflowFilters: test.filters}
assert.Equal(t, test.want, matchWorkflowFilters(wf, test.issue, test.trigger))
})
}
}
func TestCanManageWorkflows(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
user := func(id int64) *user_model.User { return unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: id}) }
publicOnly := func(id int64) *user_model.User {
doer := *user(id)
doer.ExtDoerData = user_model.NewTokenExtDoerData(user_model.CredentialAccessToken, 1, "public-only,write:issue")
return &doer
}
repoProject := unittest.AssertExistsAndLoadBean(t, &project_model.Project{ID: 2})
orgProject := &project_model.Project{Title: "org", OwnerID: 3, Type: project_model.TypeOrganization}
userProject := &project_model.Project{Title: "user", OwnerID: 2, Type: project_model.TypeIndividual}
for _, tc := range []struct {
project *project_model.Project
doer *user_model.User
want bool
}{
{repoProject, user(4), true},
{repoProject, user(5), false},
{orgProject, user(2), true},
{orgProject, user(1), true},
{orgProject, user(4), false},
{orgProject, publicOnly(2), false},
{userProject, user(2), true},
{userProject, user(1), false},
{userProject, publicOnly(2), false},
} {
allowed, err := CanManageWorkflows(t.Context(), tc.doer, tc.project)
require.NoError(t, err)
assert.Equal(t, tc.want, allowed, "project %q, user %d", tc.project.Title, tc.doer.ID)
}
for _, removedUnit := range []unit.Type{unit.TypeIssues, unit.TypePullRequests} {
require.NoError(t, unittest.PrepareTestDatabase())
_, err := db.Exec(t.Context(), "UPDATE team SET authorize = ? WHERE id = ?", perm.AccessModeNone, 2)
require.NoError(t, err)
_, err = db.DeleteByBean(t.Context(), &organization.TeamUnit{TeamID: 2, Type: removedUnit})
require.NoError(t, err)
allowed, err := CanManageWorkflows(t.Context(), user(4), repoProject)
require.NoError(t, err)
assert.False(t, allowed, removedUnit.LogString())
}
}
func TestCreateUpdateWorkflow(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
defer test.MockVariableValue(&setting.Audit.RecordOutput, setting.AuditRecordOutputDatabase)()
ctx := t.Context()
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
reader := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
project := unittest.AssertExistsAndLoadBean(t, &project_model.Project{ID: 1})
t.Run("Rejects", func(t *testing.T) {
tests := []struct {
name string
doer *user_model.User
opts api.CreateProjectWorkflowOption
code int
}{
{"unknown event", owner, api.CreateProjectWorkflowOption{Event: "bogus", Actions: api.ProjectWorkflowActions{ColumnID: 2}}, 422},
{"no actions", owner, api.CreateProjectWorkflowOption{Event: "item_added_to_project"}, 422},
{"other project column", owner, api.CreateProjectWorkflowOption{Event: "item_added_to_project", Actions: api.ProjectWorkflowActions{ColumnID: 4}}, 422},
{"other owner label", owner, api.CreateProjectWorkflowOption{Event: "item_added_to_project", Actions: api.ProjectWorkflowActions{AddLabelIDs: []int64{3}}}, 422},
{"unsupported filter", owner, api.CreateProjectWorkflowOption{Event: "item_added_to_project", Filters: api.ProjectWorkflowFilters{SourceColumnID: 1}, Actions: api.ProjectWorkflowActions{ColumnID: 2}}, 422},
{"unsupported action", owner, api.CreateProjectWorkflowOption{Event: "item_removed_from_project", Actions: api.ProjectWorkflowActions{ColumnID: 2}}, 422},
{"unknown issue state", owner, api.CreateProjectWorkflowOption{Event: "item_added_to_project", Actions: api.ProjectWorkflowActions{IssueState: "bogus"}}, 422},
{"reopen pull requests", owner, api.CreateProjectWorkflowOption{Event: "item_added_to_project", Filters: api.ProjectWorkflowFilters{IssueType: "pull_request"}, Actions: api.ProjectWorkflowActions{IssueState: "reopen"}}, 422},
{"unknown issue type", owner, api.CreateProjectWorkflowOption{Event: "item_added_to_project", Filters: api.ProjectWorkflowFilters{IssueType: "bogus"}, Actions: api.ProjectWorkflowActions{ColumnID: 2}}, 422},
{"without issue and pull request write", reader, api.CreateProjectWorkflowOption{Event: "item_added_to_project", Actions: api.ProjectWorkflowActions{ColumnID: 2}}, 403},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := CreateWorkflow(ctx, test.doer, project, &test.opts)
_, code := util.ErrorUnwrapForUser(err)
assert.Equal(t, test.code, code)
})
}
})
t.Run("PartialUpdate", func(t *testing.T) {
wf, err := CreateWorkflow(ctx, owner, project, &api.CreateProjectWorkflowOption{
Event: "item_added_to_project",
Filters: api.ProjectWorkflowFilters{IssueType: "issue", LabelIDs: []int64{2, 1, 2}},
Actions: api.ProjectWorkflowActions{ColumnID: 2},
})
require.NoError(t, err)
require.NoError(t, UpdateWorkflow(ctx, owner, project, wf, &api.EditProjectWorkflowOption{Enabled: new(false)}))
require.NoError(t, UpdateWorkflow(ctx, owner, project, wf, &api.EditProjectWorkflowOption{Actions: &api.ProjectWorkflowActions{AddLabelIDs: []int64{1}}}))
unittest.AssertExistsAndLoadBean(t, &audit_model.Event{Action: audit_model.ProjectWorkflowCreate, ActorID: owner.ID, ScopeType: audit_model.ScopeRepository, ScopeID: project.RepoID})
stored := unittest.AssertExistsAndLoadBean(t, &project_model.Workflow{ID: wf.ID})
assert.False(t, stored.Enabled)
assert.Equal(t, project_model.WorkflowFilters{IssueType: "issue", LabelIDs: []int64{1, 2}}, stored.WorkflowFilters)
assert.Equal(t, project_model.WorkflowActions{AddLabelIDs: []int64{1}}, stored.WorkflowActions)
assert.ErrorIs(t, UpdateWorkflow(ctx, reader, project, stored, &api.EditProjectWorkflowOption{Enabled: new(true)}), util.ErrPermissionDenied)
assert.ErrorIs(t, DeleteWorkflow(ctx, reader, project, stored), util.ErrPermissionDenied)
require.NoError(t, DeleteWorkflow(ctx, owner, project, stored))
unittest.AssertNotExistsBean(t, &project_model.Workflow{ID: wf.ID})
for _, action := range []audit_model.Action{audit_model.ProjectWorkflowUpdate, audit_model.ProjectWorkflowDelete} {
unittest.AssertExistsAndLoadBean(t, &audit_model.Event{Action: action, ActorID: owner.ID, ScopeType: audit_model.ScopeRepository, ScopeID: project.RepoID})
}
})
userProject := &project_model.Project{Title: "workflows", OwnerID: owner.ID, CreatorID: owner.ID, Type: project_model.TypeIndividual}
require.NoError(t, project_model.NewProject(ctx, userProject))
_, err := CreateWorkflow(ctx, owner, userProject, &api.CreateProjectWorkflowOption{Event: "item_added_to_project", Actions: api.ProjectWorkflowActions{IssueState: "close"}})
require.NoError(t, err)
event := unittest.AssertExistsAndLoadBean(t, &audit_model.Event{Action: audit_model.ProjectWorkflowCreate, ScopeType: audit_model.ScopeUser, ScopeID: userProject.OwnerID})
assert.Contains(t, event.Message, "item_added_to_project")
}
type labelChangeRecorder struct {
notify_service.NullNotifier
added [][]int64
}
func (r *labelChangeRecorder) IssueChangeLabels(_ctx context.Context, _doer *user_model.User, _issue *issues_model.Issue, added, _removed []*issues_model.Label) {
r.added = append(r.added, container.FilterSlice(added, func(l *issues_model.Label) (int64, bool) { return l.ID, true }))
}
func TestWorkflowExecution(t *testing.T) {
const labelID = 2
require.NoError(t, unittest.PrepareTestDatabase())
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
addLabel := project_model.WorkflowActions{AddLabelIDs: []int64{labelID}}
addWorkflow := func(t *testing.T, wf project_model.Workflow) {
wf.ProjectID, wf.Enabled = 1, true
require.NoError(t, project_model.CreateWorkflow(t.Context(), &wf))
}
setup := func(t *testing.T, wf project_model.Workflow) {
require.NoError(t, unittest.PrepareTestDatabase())
addWorkflow(t, wf)
}
hasLabel := func(t *testing.T, issueID int64) bool {
return unittest.GetCount(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) == 1
}
moveIssue := func(t *testing.T, ctx context.Context, issueID, columnID int64) {
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueID})
column := unittest.AssertExistsAndLoadBean(t, &project_model.Column{ID: columnID})
require.NoError(t, MoveIssueToColumn(ctx, doer, issue, column, optional.None[int64]()))
}
t.Run("ColumnChangedFromDefaultColumn", func(t *testing.T) {
addWorkflow(t, project_model.Workflow{WorkflowEvent: project_model.WorkflowEventItemColumnChanged, WorkflowFilters: project_model.WorkflowFilters{SourceColumnID: 1, TargetColumnID: 2}, WorkflowActions: addLabel})
moveIssue(t, t.Context(), 2, 2)
assert.True(t, hasLabel(t, 2))
})
t.Run("NoOpMoveWithinDefaultColumnAndNoCascadeFromWorkflows", func(t *testing.T) {
setup(t, project_model.Workflow{WorkflowEvent: project_model.WorkflowEventItemColumnChanged, WorkflowActions: addLabel})
moveIssue(t, t.Context(), 2, 1)
assert.False(t, hasLabel(t, 2))
unittest.AssertNotExistsBean(t, &issues_model.Comment{IssueID: 2, Type: issues_model.CommentTypeProjectColumn})
moveIssue(t, issues_model.WithProjectWorkflow(t.Context(), project_model.WorkflowEventItemAddedToProject), 1, 2)
assert.False(t, hasLabel(t, 1))
})
t.Run("LabelsAndClosesPullRequestAsTriggeringReader", func(t *testing.T) {
setup(t, project_model.Workflow{WorkflowEvent: project_model.WorkflowEventItemColumnChanged, WorkflowActions: project_model.WorkflowActions{AddLabelIDs: []int64{labelID}, IssueState: project_model.WorkflowIssueStateClose}})
reader := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
require.NoError(t, MoveIssueToColumn(t.Context(), reader, unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}), unittest.AssertExistsAndLoadBean(t, &project_model.Column{ID: 3}), optional.None[int64]()))
assert.True(t, hasLabel(t, 3))
assert.True(t, unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}).IsClosed)
for _, commentType := range []issues_model.CommentType{issues_model.CommentTypeLabel, issues_model.CommentTypeClose} {
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: 3, Type: commentType})
assert.Equal(t, reader.ID, comment.PosterID)
assert.Equal(t, &issues_model.CommentMetaData{
ProjectWorkflowEvent: project_model.WorkflowEventItemColumnChanged,
SpecialDoerName: issues_model.SpecialDoerNameProjectWorkflow,
}, comment.CommentMetaData)
}
})
t.Run("ArchivedRepository", func(t *testing.T) {
setup(t, project_model.Workflow{WorkflowEvent: project_model.WorkflowEventItemClosed, WorkflowActions: addLabel})
require.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(t.Context(), &repo_model.Repository{ID: 1, IsArchived: true}, "is_archived"))
(&workflowNotifier{}).IssueChangeStatus(t.Context(), doer, "", unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}), nil, true)
assert.False(t, hasLabel(t, 1))
})
t.Run("ReopenSkipsPullRequests", func(t *testing.T) {
setup(t, project_model.Workflow{WorkflowEvent: project_model.WorkflowEventItemColumnChanged, WorkflowActions: project_model.WorkflowActions{IssueState: project_model.WorkflowIssueStateReopen}})
require.NoError(t, issues_model.UpdateIssueCols(t.Context(), &issues_model.Issue{ID: 3, IsClosed: true}, "is_closed"))
moveIssue(t, t.Context(), 3, 1)
moveIssue(t, t.Context(), 5, 1)
assert.True(t, unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}).IsClosed)
assert.False(t, unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 5}).IsClosed)
})
t.Run("OfficialReviewsOnly", func(t *testing.T) {
setup(t, project_model.Workflow{WorkflowEvent: project_model.WorkflowEventCodeReviewApproved, WorkflowActions: addLabel})
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: 3})
review := &issues_model.Review{Type: issues_model.ReviewTypeApprove, Reviewer: doer}
(&workflowNotifier{}).PullRequestReview(t.Context(), pr, review, nil, nil)
assert.False(t, hasLabel(t, 3))
review.Official = true
(&workflowNotifier{}).PullRequestReview(t.Context(), pr, review, nil, nil)
assert.True(t, hasLabel(t, 3))
})
t.Run("AddedToAndRemovedFromProject", func(t *testing.T) {
setup(t, project_model.Workflow{WorkflowEvent: project_model.WorkflowEventItemAddedToProject, WorkflowActions: addLabel})
addWorkflow(t, project_model.Workflow{WorkflowEvent: project_model.WorkflowEventItemRemovedFromProject, WorkflowActions: project_model.WorkflowActions{RemoveLabelIDs: []int64{labelID}}})
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 11})
column := unittest.AssertExistsAndLoadBean(t, &project_model.Column{ID: 2})
require.NoError(t, AddIssueToColumn(t.Context(), doer, issue, column))
assert.True(t, hasLabel(t, 11))
require.NoError(t, RemoveIssueFromColumn(t.Context(), doer, issue, column))
assert.False(t, hasLabel(t, 11))
})
t.Run("NewItemInProject", func(t *testing.T) {
setup(t, project_model.Workflow{WorkflowEvent: project_model.WorkflowEventItemAddedToProject, WorkflowActions: project_model.WorkflowActions{ColumnID: 3, AddLabelIDs: []int64{labelID}}})
*labelChanges = labelChangeRecorder{}
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
issue := &issues_model.Issue{RepoID: repo.ID, PosterID: doer.ID, Title: "new item"}
require.NoError(t, issue_service.NewIssue(t.Context(), repo, issue, []int64{1}, nil, nil, []int64{1}))
assert.Equal(t, [][]int64{{1}, {labelID}}, labelChanges.added)
assert.EqualValues(t, 3, unittest.AssertExistsAndLoadBean(t, &project_model.ProjectIssue{IssueID: issue.ID}).ProjectColumnID)
})
}