mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09:00
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:
1 parent
ce7364e215
commit
8abda62312
388 files changed
+18410
-29561
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 := `
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user