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.
This commit is contained in:
silverwind authored and GitHub committed 2026-10-24 13:48:19 +00:00
1 parent ce7364e215
commit 8abda62312
388 files changed
+18410 -29561

No files matched your search

+1 -9
View File
@@ -212,15 +212,7 @@ func TestStopEndlessTasksSkipsCancelling(t *testing.T) {
var seq int64
newTaskWithJob := func(status actions_model.Status) *actions_model.ActionTask {
seq++
run := &actions_model.ActionRun{
RepoID: 1, OwnerID: 2, TriggerUserID: 2, WorkflowID: "test.yml",
Index: 99500 + seq, Ref: "refs/heads/main", Status: actions_model.StatusRunning,
}
require.NoError(t, db.Insert(t.Context(), run))
attempt := &actions_model.ActionRunAttempt{
RepoID: run.RepoID, RunID: run.ID, Attempt: 1, TriggerUserID: run.TriggerUserID, Status: actions_model.StatusRunning,
}
require.NoError(t, db.Insert(t.Context(), attempt))
run, attempt := createRunAttempt(t, 99500+seq, "", actions_model.StatusRunning)
job := &actions_model.ActionRunJob{
RunID: run.ID, RunAttemptID: attempt.ID, AttemptJobID: 1, RepoID: run.RepoID, OwnerID: run.OwnerID,
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0", Name: "j", JobID: "j", Status: status,
+16 -26
View File
@@ -439,32 +439,22 @@ func TestGenerateGiteaContextRefProtected(t *testing.T) {
NamePattern: "v*",
}))
gitCtx := GenerateGiteaContext(t.Context(), &actions_model.ActionRun{
RepoID: repo.ID,
Repo: repo,
TriggerUser: &user_model.User{Name: "test-user"},
Ref: "refs/heads/master",
}, nil, nil)
assert.Equal(t, true, gitCtx["ref_protected"])
tagCtx := GenerateGiteaContext(t.Context(), &actions_model.ActionRun{
RepoID: repo.ID,
Repo: repo,
TriggerUser: &user_model.User{Name: "test-user"},
Ref: "refs/tags/v1.0.0",
}, nil, nil)
assert.Equal(t, true, tagCtx["ref_protected"])
unprotectedTagCtx := GenerateGiteaContext(t.Context(), &actions_model.ActionRun{
RepoID: repo.ID,
Repo: repo,
TriggerUser: &user_model.User{Name: "test-user"},
Ref: "refs/tags/other",
}, nil, nil)
assert.Equal(t, false, unprotectedTagCtx["ref_protected"])
for _, tc := range []struct {
ref string
protected bool
}{
{"refs/heads/master", true},
{"refs/tags/v1.0.0", true},
{"refs/tags/other", false},
} {
gitCtx := GenerateGiteaContext(t.Context(), &actions_model.ActionRun{
RepoID: repo.ID,
Repo: repo,
TriggerUser: &user_model.User{Name: "test-user"},
Ref: tc.ref,
}, nil, nil)
assert.Equal(t, tc.protected, gitCtx["ref_protected"])
}
}
// TestGenerateGiteaContext_NilAttempt verifies that, with no explicit attempt,
+26 -43
View File
@@ -99,47 +99,30 @@ func TestPullRequestTargetBaseSHA(t *testing.T) {
return string(payload)
}
t.Run("pull_request_target with base SHA", func(t *testing.T) {
run := &actions_model.ActionRun{
Event: webhook_module.HookEventPullRequest,
TriggerEvent: actions_module.GithubEventPullRequestTarget,
EventPayload: prPayload("base-sha"),
}
got, ok := pullRequestTargetBaseSHA(run)
assert.True(t, ok)
assert.Equal(t, "base-sha", got)
})
t.Run("non pull_request_target trigger", func(t *testing.T) {
run := &actions_model.ActionRun{
Event: webhook_module.HookEventPullRequest,
TriggerEvent: actions_module.GithubEventPullRequest,
EventPayload: prPayload("base-sha"),
}
got, ok := pullRequestTargetBaseSHA(run)
assert.False(t, ok)
assert.Empty(t, got)
})
t.Run("missing base SHA", func(t *testing.T) {
run := &actions_model.ActionRun{
Event: webhook_module.HookEventPullRequest,
TriggerEvent: actions_module.GithubEventPullRequestTarget,
EventPayload: prPayload(""),
}
got, ok := pullRequestTargetBaseSHA(run)
assert.False(t, ok)
assert.Empty(t, got)
})
t.Run("invalid payload", func(t *testing.T) {
run := &actions_model.ActionRun{
Event: webhook_module.HookEventPullRequest,
TriggerEvent: actions_module.GithubEventPullRequestTarget,
EventPayload: "{",
}
got, ok := pullRequestTargetBaseSHA(run)
assert.False(t, ok)
assert.Empty(t, got)
})
for _, test := range []struct {
name string
triggerEvent string
payload string
wantSHA string
}{
{"pull_request_target with base SHA", actions_module.GithubEventPullRequestTarget, prPayload("base-sha"), "base-sha"},
{"non pull_request_target trigger", actions_module.GithubEventPullRequest, prPayload("base-sha"), ""},
{"missing base SHA", actions_module.GithubEventPullRequestTarget, prPayload(""), ""},
{"invalid payload", actions_module.GithubEventPullRequestTarget, "{", ""},
} {
t.Run(test.name, func(t *testing.T) {
got, ok := pullRequestTargetBaseSHA(&actions_model.ActionRun{
Event: webhook_module.HookEventPullRequest,
TriggerEvent: test.triggerEvent,
EventPayload: test.payload,
})
if test.wantSHA != "" {
assert.True(t, ok)
assert.Equal(t, test.wantSHA, got)
} else {
assert.False(t, ok)
assert.Empty(t, got)
}
})
}
}
+87 -125
View File
@@ -431,94 +431,95 @@ func Test_maxParallelConverges(t *testing.T) {
// ConcurrencyGroup has already been checked at the run level, the same group is not
// re-checked for individual jobs.
func Test_checkRunConcurrency_NoDuplicateConcurrencyGroupCheck(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
for _, test := range []struct {
name string
index int64
status actions_model.Status
}{
// Run A: the triggering run of attempt A. It is done, so it no longer holds "test-cg", which is what lets checkRunConcurrency wake the blocked waiter.
// Attempt A: a done attempt of run A with concurrency group "test-cg"
{name: "free group", index: 9901, status: actions_model.StatusSuccess},
// Test_checkRunConcurrency_HeldGroupDoesNotWake verifies that only an unoccupied concurrency group can wake up a blocked run/job.
// Run A holds "test-cg": its attempt is still running.
{name: "held group", index: 9911, status: actions_model.StatusRunning},
} {
t.Run(test.name, func(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
insertRun := func(index int64, status actions_model.Status) (*actions_model.ActionRun, *actions_model.ActionRunAttempt) {
run := &actions_model.ActionRun{
RepoID: 4,
OwnerID: 1,
TriggerUserID: 1,
WorkflowID: "test.yml",
Index: index,
Ref: "refs/heads/main",
Status: status,
}
assert.NoError(t, db.Insert(ctx, run))
attempt := &actions_model.ActionRunAttempt{
RepoID: 4,
RunID: run.ID,
Attempt: 1,
Status: status,
ConcurrencyGroup: "test-cg",
}
assert.NoError(t, db.Insert(ctx, attempt))
_, err := db.Exec(ctx, "UPDATE `action_run` SET latest_attempt_id = ? WHERE id = ?", attempt.ID, run.ID)
assert.NoError(t, err)
return run, attempt
}
// Run A: the triggering run of attempt A. It is done, so it no longer holds "test-cg", which is what lets checkRunConcurrency wake the blocked waiter.
runA := &actions_model.ActionRun{
RepoID: 4,
OwnerID: 1,
TriggerUserID: 1,
WorkflowID: "test.yml",
Index: 9901,
Ref: "refs/heads/main",
Status: actions_model.StatusSuccess,
runA, runAAttempt := insertRun(test.index, test.status)
if test.status == actions_model.StatusSuccess {
// A done job for run A with the same ConcurrencyGroup.
// This triggers the job-level concurrency check in checkRunConcurrency.
assert.NoError(t, db.Insert(ctx, &actions_model.ActionRunJob{
RunID: runA.ID,
RunAttemptID: runAAttempt.ID,
AttemptJobID: 1,
RepoID: 4,
OwnerID: 1,
JobID: "job1",
Name: "job1",
Status: actions_model.StatusSuccess,
ConcurrencyGroup: "test-cg",
}))
}
// Run B: a run blocked by concurrency
// Run B is blocked on the same group.
// Attempt B: an blocked attempt of run B
runB, runBAttempt := insertRun(test.index+1, actions_model.StatusBlocked)
if test.status == actions_model.StatusSuccess {
// A blocked job belonging to run B (no job-level concurrency group).
assert.NoError(t, db.Insert(ctx, &actions_model.ActionRunJob{
RunID: runB.ID,
RunAttemptID: runBAttempt.ID,
AttemptJobID: 1,
RepoID: 4,
OwnerID: 1,
JobID: "job1",
Name: "job1",
Status: actions_model.StatusBlocked,
}))
}
runA, _, _ = db.GetByID[actions_model.ActionRun](ctx, runA.ID)
result, err := checkRunConcurrency(ctx, runA)
assert.NoError(t, err)
if test.status == actions_model.StatusSuccess {
// "test-cg" is free, so the single blocked waiter (run B) is collected for re-emit.
if assert.Len(t, result.RunIDsToReEmit, 1) {
assert.Equal(t, runB.ID, result.RunIDsToReEmit[0])
}
assert.Empty(t, result.Jobs)
} else {
// The group is held by run A, so run B must not be woken; A will wake it when it releases the group.
assert.Empty(t, result.RunIDsToReEmit)
}
})
}
assert.NoError(t, db.Insert(ctx, runA))
// Attempt A: a done attempt of run A with concurrency group "test-cg"
runAAttempt := &actions_model.ActionRunAttempt{
RepoID: 4,
RunID: runA.ID,
Attempt: 1,
Status: actions_model.StatusSuccess,
ConcurrencyGroup: "test-cg",
}
assert.NoError(t, db.Insert(ctx, runAAttempt))
_, err := db.Exec(t.Context(), "UPDATE `action_run` SET latest_attempt_id = ? WHERE id = ?", runAAttempt.ID, runA.ID)
assert.NoError(t, err)
// A done job for run A with the same ConcurrencyGroup.
// This triggers the job-level concurrency check in checkRunConcurrency.
jobADone := &actions_model.ActionRunJob{
RunID: runA.ID,
RunAttemptID: runAAttempt.ID,
AttemptJobID: 1,
RepoID: 4,
OwnerID: 1,
JobID: "job1",
Name: "job1",
Status: actions_model.StatusSuccess,
ConcurrencyGroup: "test-cg",
}
assert.NoError(t, db.Insert(ctx, jobADone))
// Run B: a run blocked by concurrency
runB := &actions_model.ActionRun{
RepoID: 4,
OwnerID: 1,
TriggerUserID: 1,
WorkflowID: "test.yml",
Index: 9902,
Ref: "refs/heads/main",
Status: actions_model.StatusBlocked,
}
assert.NoError(t, db.Insert(ctx, runB))
// Attempt B: an blocked attempt of run B
runBAttempt := &actions_model.ActionRunAttempt{
RepoID: 4,
RunID: runB.ID,
Attempt: 1,
Status: actions_model.StatusBlocked,
ConcurrencyGroup: "test-cg",
}
assert.NoError(t, db.Insert(ctx, runBAttempt))
_, err = db.Exec(t.Context(), "UPDATE `action_run` SET latest_attempt_id = ? WHERE id = ?", runBAttempt.ID, runB.ID)
assert.NoError(t, err)
// A blocked job belonging to run B (no job-level concurrency group).
jobBBlocked := &actions_model.ActionRunJob{
RunID: runB.ID,
RunAttemptID: runBAttempt.ID,
AttemptJobID: 1,
RepoID: 4,
OwnerID: 1,
JobID: "job1",
Name: "job1",
Status: actions_model.StatusBlocked,
}
assert.NoError(t, db.Insert(ctx, jobBBlocked))
runA, _, _ = db.GetByID[actions_model.ActionRun](t.Context(), runA.ID)
result, err := checkRunConcurrency(ctx, runA)
assert.NoError(t, err)
// "test-cg" is free, so the single blocked waiter (run B) is collected for re-emit.
if assert.Len(t, result.RunIDsToReEmit, 1) {
assert.Equal(t, runB.ID, result.RunIDsToReEmit[0])
}
assert.Empty(t, result.Jobs)
}
// Test_checkJobsOfCurrentRunAttempt_RunLevelConcurrencyKeepsJobsBlocked verifies that
@@ -732,45 +733,6 @@ func Test_checkJobsOfCurrentRunAttempt_SkippedCallerIsUpdated(t *testing.T) {
assert.Equal(t, actions_model.StatusWaiting, unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: after.ID}).Status)
}
// Test_checkRunConcurrency_HeldGroupDoesNotWake verifies that only an unoccupied concurrency group can wake up a blocked run/job.
func Test_checkRunConcurrency_HeldGroupDoesNotWake(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
// Run A holds "test-cg": its attempt is still running.
runA := &actions_model.ActionRun{
RepoID: 4, OwnerID: 1, TriggerUserID: 1, WorkflowID: "test.yml",
Index: 9911, Ref: "refs/heads/main", Status: actions_model.StatusRunning,
}
assert.NoError(t, db.Insert(ctx, runA))
runAAttempt := &actions_model.ActionRunAttempt{
RepoID: 4, RunID: runA.ID, Attempt: 1, Status: actions_model.StatusRunning, ConcurrencyGroup: "test-cg",
}
assert.NoError(t, db.Insert(ctx, runAAttempt))
_, err := db.Exec(ctx, "UPDATE `action_run` SET latest_attempt_id = ? WHERE id = ?", runAAttempt.ID, runA.ID)
assert.NoError(t, err)
// Run B is blocked on the same group.
runB := &actions_model.ActionRun{
RepoID: 4, OwnerID: 1, TriggerUserID: 1, WorkflowID: "test.yml",
Index: 9912, Ref: "refs/heads/main", Status: actions_model.StatusBlocked,
}
assert.NoError(t, db.Insert(ctx, runB))
runBAttempt := &actions_model.ActionRunAttempt{
RepoID: 4, RunID: runB.ID, Attempt: 1, Status: actions_model.StatusBlocked, ConcurrencyGroup: "test-cg",
}
assert.NoError(t, db.Insert(ctx, runBAttempt))
_, err = db.Exec(ctx, "UPDATE `action_run` SET latest_attempt_id = ? WHERE id = ?", runBAttempt.ID, runB.ID)
assert.NoError(t, err)
runA, _, _ = db.GetByID[actions_model.ActionRun](ctx, runA.ID)
result, err := checkRunConcurrency(ctx, runA)
assert.NoError(t, err)
// The group is held by run A, so run B must not be woken; A will wake it when it releases the group.
assert.Empty(t, result.RunIDsToReEmit)
}
// Test_findConcurrencyWaiterToWake covers the finder's contract: it skips the run being processed (excludeRunID),
// returns another blocked waiter when the group is free, and returns 0 while the group is still held.
func Test_findConcurrencyWaiterToWake(t *testing.T) {
+21 -35
View File
@@ -41,41 +41,27 @@ func TestIfNeedApproval(t *testing.T) {
normalUser := &user_model.User{ID: 10}
restrictedUser := &user_model.User{ID: 11, IsRestricted: true}
t.Run("not a fork PR never needs approval", func(t *testing.T) {
need, err := ifNeedApprovalWith(t.Context(), nonForkRun, repo, normalUser, alwaysWrite, hasMerged)
require.NoError(t, err)
assert.False(t, need)
})
t.Run("pull_request_target never needs approval even when fork", func(t *testing.T) {
need, err := ifNeedApprovalWith(t.Context(), prTargetRun, repo, normalUser, alwaysWrite, hasMerged)
require.NoError(t, err)
assert.False(t, need)
})
t.Run("restricted user always needs approval", func(t *testing.T) {
need, err := ifNeedApprovalWith(t.Context(), forkRun, repo, restrictedUser, alwaysWrite, hasMerged)
require.NoError(t, err)
assert.True(t, need)
})
t.Run("fork PR with write permission does not need approval", func(t *testing.T) {
need, err := ifNeedApprovalWith(t.Context(), forkRun, repo, normalUser, alwaysWrite, noMerged)
require.NoError(t, err)
assert.False(t, need)
})
t.Run("fork PR with merged PR but no write permission does not need approval", func(t *testing.T) {
need, err := ifNeedApprovalWith(t.Context(), forkRun, repo, normalUser, neverWrite, hasMerged)
require.NoError(t, err)
assert.False(t, need)
})
t.Run("fork PR with no write and no merged PR needs approval", func(t *testing.T) {
need, err := ifNeedApprovalWith(t.Context(), forkRun, repo, normalUser, neverWrite, noMerged)
require.NoError(t, err)
assert.True(t, need)
})
for _, tt := range []struct {
name string
run *actions_model.ActionRun
user *user_model.User
canWrite func(context.Context, *repo_model.Repository, *user_model.User) (bool, error)
hasMerged func(context.Context, int64, int64) (bool, error)
check assert.BoolAssertionFunc
}{
{"not a fork PR never needs approval", nonForkRun, normalUser, alwaysWrite, hasMerged, assert.False},
{"pull_request_target never needs approval even when fork", prTargetRun, normalUser, alwaysWrite, hasMerged, assert.False},
{"restricted user always needs approval", forkRun, restrictedUser, alwaysWrite, hasMerged, assert.True},
{"fork PR with write permission does not need approval", forkRun, normalUser, alwaysWrite, noMerged, assert.False},
{"fork PR with merged PR but no write permission does not need approval", forkRun, normalUser, neverWrite, hasMerged, assert.False},
{"fork PR with no write and no merged PR needs approval", forkRun, normalUser, neverWrite, noMerged, assert.True},
} {
t.Run(tt.name, func(t *testing.T) {
need, err := ifNeedApprovalWith(t.Context(), tt.run, repo, tt.user, tt.canWrite, tt.hasMerged)
require.NoError(t, err)
tt.check(t, need)
})
}
t.Run("canWriteActions error is propagated", func(t *testing.T) {
failWrite := func(_ context.Context, _ *repo_model.Repository, _ *user_model.User) (bool, error) {
+46 -61
View File
@@ -16,21 +16,52 @@ import (
"go.yaml.in/yaml/v4"
)
func TestParseRawPermissions_ReadAll(t *testing.T) {
var rawPerms yaml.Node
err := yaml.Unmarshal([]byte(`read-all`), &rawPerms)
assert.NoError(t, err)
result := parseRawPermissionsExplicit(&rawPerms)
require.NotNil(t, result)
assert.Equal(t, perm.AccessModeRead, result.UnitAccessModes[unit.TypeCode])
assert.Equal(t, perm.AccessModeRead, result.UnitAccessModes[unit.TypeIssues])
assert.Equal(t, perm.AccessModeRead, result.UnitAccessModes[unit.TypePullRequests])
assert.Equal(t, perm.AccessModeRead, result.UnitAccessModes[unit.TypePackages])
assert.Equal(t, perm.AccessModeRead, result.UnitAccessModes[unit.TypeActions])
assert.Equal(t, perm.AccessModeRead, result.UnitAccessModes[unit.TypeWiki])
assert.Equal(t, perm.AccessModeRead, result.UnitAccessModes[unit.TypeProjects])
func TestParseRawPermissions_Scopes(t *testing.T) {
for _, tt := range []struct {
name string
input string
expectedModes [7]perm.AccessMode
idToken bool
}{
{"ReadAll", "read-all", [7]perm.AccessMode{
perm.AccessModeRead, perm.AccessModeRead, perm.AccessModeRead,
perm.AccessModeRead, perm.AccessModeRead, perm.AccessModeRead, perm.AccessModeRead,
}, false},
{"WriteAll", "write-all", [7]perm.AccessMode{
perm.AccessModeWrite, perm.AccessModeWrite, perm.AccessModeWrite,
perm.AccessModeWrite, perm.AccessModeWrite, perm.AccessModeWrite, perm.AccessModeWrite,
}, true},
{"IndividualScopes", `
contents: write
issues: read
pull-requests: none
packages: write
actions: read
wiki: write
projects: none
id-token: write
`, [7]perm.AccessMode{
perm.AccessModeWrite, perm.AccessModeRead, perm.AccessModeNone,
perm.AccessModeWrite, perm.AccessModeRead, perm.AccessModeWrite, perm.AccessModeNone,
}, true},
} {
t.Run(tt.name, func(t *testing.T) {
var rawPerms yaml.Node
err := yaml.Unmarshal([]byte(tt.input), &rawPerms)
assert.NoError(t, err)
result := parseRawPermissionsExplicit(&rawPerms)
require.NotNil(t, result)
for i, unitType := range []unit.Type{
unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypePackages,
unit.TypeActions, unit.TypeWiki, unit.TypeProjects,
} {
assert.Equal(t, tt.expectedModes[i], result.UnitAccessModes[unitType])
}
if tt.idToken {
assert.True(t, result.IDToken)
}
})
}
}
// TestParseRawPermissions_GithubScopes verifies that all scopes that github supports are accounted for
@@ -63,52 +94,6 @@ statuses: read`
// No asserts for permissions set on purpose
}
func TestParseRawPermissions_WriteAll(t *testing.T) {
var rawPerms yaml.Node
err := yaml.Unmarshal([]byte(`write-all`), &rawPerms)
assert.NoError(t, err)
result := parseRawPermissionsExplicit(&rawPerms)
require.NotNil(t, result)
assert.Equal(t, perm.AccessModeWrite, result.UnitAccessModes[unit.TypeCode])
assert.Equal(t, perm.AccessModeWrite, result.UnitAccessModes[unit.TypeIssues])
assert.Equal(t, perm.AccessModeWrite, result.UnitAccessModes[unit.TypePullRequests])
assert.Equal(t, perm.AccessModeWrite, result.UnitAccessModes[unit.TypePackages])
assert.Equal(t, perm.AccessModeWrite, result.UnitAccessModes[unit.TypeActions])
assert.Equal(t, perm.AccessModeWrite, result.UnitAccessModes[unit.TypeWiki])
assert.Equal(t, perm.AccessModeWrite, result.UnitAccessModes[unit.TypeProjects])
assert.True(t, result.IDToken)
}
func TestParseRawPermissions_IndividualScopes(t *testing.T) {
yamlContent := `
contents: write
issues: read
pull-requests: none
packages: write
actions: read
wiki: write
projects: none
id-token: write
`
var rawPerms yaml.Node
err := yaml.Unmarshal([]byte(yamlContent), &rawPerms)
assert.NoError(t, err)
result := parseRawPermissionsExplicit(&rawPerms)
require.NotNil(t, result)
assert.Equal(t, perm.AccessModeWrite, result.UnitAccessModes[unit.TypeCode])
assert.Equal(t, perm.AccessModeRead, result.UnitAccessModes[unit.TypeIssues])
assert.Equal(t, perm.AccessModeNone, result.UnitAccessModes[unit.TypePullRequests])
assert.Equal(t, perm.AccessModeWrite, result.UnitAccessModes[unit.TypePackages])
assert.Equal(t, perm.AccessModeRead, result.UnitAccessModes[unit.TypeActions])
assert.Equal(t, perm.AccessModeWrite, result.UnitAccessModes[unit.TypeWiki])
assert.Equal(t, perm.AccessModeNone, result.UnitAccessModes[unit.TypeProjects])
assert.True(t, result.IDToken)
}
func TestParseRawPermissions_Priority(t *testing.T) {
t.Run("granular-wins-over-contents", func(t *testing.T) {
yamlContent := `
+38 -65
View File
@@ -25,63 +25,38 @@ import (
)
func TestCheckCallerChain_Cycle(t *testing.T) {
t.Run("DirectCycle", func(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
for _, testCase := range []struct {
name string
uses []string
sourceRepoID int64
error string
}{
// A -> A: leaf's CallUses matches its direct parent's.
chain := buildCallerChain(t,
"./.gitea/workflows/a.yml",
"./.gitea/workflows/a.yml",
)
err := checkCallerChain(t.Context(), chain[len(chain)-1])
assert.ErrorContains(t, err, "cycle detected")
})
t.Run("IndirectCycle", func(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
{"DirectCycle", []string{"./.gitea/workflows/a.yml", "./.gitea/workflows/a.yml"}, 0, "cycle detected"},
// A -> B -> A: leaf's CallUses matches its grandparent's.
chain := buildCallerChain(t,
"./.gitea/workflows/a.yml",
"./.gitea/workflows/b.yml",
"./.gitea/workflows/a.yml",
)
err := checkCallerChain(t.Context(), chain[len(chain)-1])
assert.ErrorContains(t, err, "cycle detected")
})
t.Run("MixedPrefixCycle", func(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
{"IndirectCycle", []string{"./.gitea/workflows/a.yml", "./.gitea/workflows/b.yml", "./.gitea/workflows/a.yml"}, 0, "cycle detected"},
// A -> A written with both same-repo prefixes: they name the same file.
chain := buildCallerChain(t,
"./.gitea/workflows/a.yml",
"$/.gitea/workflows/a.yml",
)
err := checkCallerChain(t.Context(), chain[len(chain)-1])
assert.ErrorContains(t, err, "cycle detected")
assert.Equal(t, canonicalCallUses(&actions_model.ActionRunJob{CallUses: "owner/repo/.gitea/workflows/a.yml@v1"}), canonicalCallUses(&actions_model.ActionRunJob{CallUses: "self:owner/repo/.gitea/workflows/a.yml@v1"}))
})
t.Run("NoCycle", func(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
{"MixedPrefixCycle", []string{"./.gitea/workflows/a.yml", "$/.gitea/workflows/a.yml"}, 0, "cycle detected"},
// Sanity: linear chain with distinct CallUses must not trip cycle detection.
chain := buildCallerChain(t,
"./.gitea/workflows/a.yml",
"./.gitea/workflows/b.yml",
"./.gitea/workflows/c.yml",
)
require.NoError(t, checkCallerChain(t.Context(), chain[len(chain)-1]))
})
t.Run("SameLocalPathInOtherRepo", func(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
chain := buildCallerChain(t,
"./.gitea/workflows/a.yml",
"owner/lib/.gitea/workflows/lib.yml@v1",
"./.gitea/workflows/a.yml",
)
leaf := chain[len(chain)-1]
leaf.WorkflowSourceRepoID = 2
require.NoError(t, checkCallerChain(t.Context(), leaf))
})
{"NoCycle", []string{"./.gitea/workflows/a.yml", "./.gitea/workflows/b.yml", "./.gitea/workflows/c.yml"}, 0, ""},
{"SameLocalPathInOtherRepo", []string{"./.gitea/workflows/a.yml", "owner/lib/.gitea/workflows/lib.yml@v1", "./.gitea/workflows/a.yml"}, 2, ""},
} {
t.Run(testCase.name, func(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
chain := buildCallerChain(t, testCase.uses...)
leaf := chain[len(chain)-1]
leaf.WorkflowSourceRepoID = testCase.sourceRepoID
err := checkCallerChain(t.Context(), leaf)
if testCase.error == "" {
require.NoError(t, err)
} else {
assert.ErrorContains(t, err, testCase.error)
}
if testCase.name == "MixedPrefixCycle" {
assert.Equal(t, canonicalCallUses(&actions_model.ActionRunJob{CallUses: "owner/repo/.gitea/workflows/a.yml@v1"}), canonicalCallUses(&actions_model.ActionRunJob{CallUses: "self:owner/repo/.gitea/workflows/a.yml@v1"}))
}
})
}
t.Run("ResolvedIdentityCycle", func(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
@@ -205,20 +180,18 @@ func TestResolveUses(t *testing.T) {
t.Run("DirectoryAllowlist", func(t *testing.T) {
// SCOPED_WORKFLOW_DIRS is allowed (local and cross-repo).
ref, err := ResolveUses(ctx, "./.gitea/scoped_workflows/lib.yml")
require.NoError(t, err)
assert.Equal(t, ".gitea/scoped_workflows/lib.yml", ref.Path)
ref, err = ResolveUses(ctx, "owner/repo/.gitea/scoped_workflows/lib.yml@v1")
require.NoError(t, err)
assert.Equal(t, ".gitea/scoped_workflows/lib.yml", ref.Path)
ref, err = ResolveUses(ctx, "self:owner/repo/.gitea/scoped_workflows/lib.yml@v1")
require.NoError(t, err)
assert.Equal(t, ".gitea/scoped_workflows/lib.yml", ref.Path)
for _, uses := range []string{
"./.gitea/scoped_workflows/lib.yml",
"owner/repo/.gitea/scoped_workflows/lib.yml@v1",
"self:owner/repo/.gitea/scoped_workflows/lib.yml@v1",
} {
ref, err := ResolveUses(ctx, uses)
require.NoError(t, err)
assert.Equal(t, ".gitea/scoped_workflows/lib.yml", ref.Path)
}
// A directory that is neither WORKFLOW_DIRS nor SCOPED_WORKFLOW_DIRS parses but is rejected by the allowlist.
_, err = ResolveUses(ctx, "./not-workflows/build.yml")
_, err := ResolveUses(ctx, "./not-workflows/build.yml")
require.Error(t, err)
_, err = ResolveUses(ctx, "owner/repo/lib/build.yml@v1")
require.Error(t, err)