2025-08-25 15:51:26 +02:00
|
|
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
|
|
|
package actions
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2025-08-26 21:29:22 -06:00
|
|
|
"html/template"
|
|
|
|
"net/http"
|
2025-08-25 15:51:26 +02:00
|
|
|
"testing"
|
|
|
|
|
|
|
|
actions_model "forgejo.org/models/actions"
|
2025-08-25 19:33:40 +02:00
|
|
|
repo_model "forgejo.org/models/repo"
|
2025-08-25 15:51:26 +02:00
|
|
|
unittest "forgejo.org/models/unittest"
|
2025-08-26 21:29:22 -06:00
|
|
|
"forgejo.org/modules/json"
|
|
|
|
"forgejo.org/modules/web"
|
2025-08-25 15:51:26 +02:00
|
|
|
"forgejo.org/services/contexttest"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
2025-08-26 21:29:22 -06:00
|
|
|
"github.com/stretchr/testify/require"
|
2025-08-25 15:51:26 +02:00
|
|
|
)
|
|
|
|
|
2025-08-25 19:33:40 +02:00
|
|
|
func Test_getRunByID(t *testing.T) {
|
|
|
|
unittest.PrepareTestEnv(t)
|
|
|
|
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 5, ID: 4})
|
|
|
|
|
|
|
|
for _, testCase := range []struct {
|
|
|
|
name string
|
|
|
|
runID int64
|
|
|
|
err string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "Found",
|
|
|
|
runID: 792,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "NotFound",
|
|
|
|
runID: 24344,
|
|
|
|
err: "no such run",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "ZeroNotFound",
|
|
|
|
runID: 0,
|
|
|
|
err: "zero is not a valid run ID",
|
|
|
|
},
|
|
|
|
} {
|
|
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
|
|
ctx, resp := contexttest.MockContext(t, fmt.Sprintf("user5/repo4/actions/runs/%v/artifacts/some-name", testCase.runID))
|
|
|
|
ctx.Repo.Repository = repo
|
|
|
|
run := getRunByID(ctx, testCase.runID)
|
|
|
|
if testCase.err == "" {
|
|
|
|
assert.NotNil(t, run)
|
|
|
|
assert.False(t, ctx.Written(), resp.Body.String())
|
|
|
|
} else {
|
|
|
|
assert.Nil(t, run)
|
|
|
|
assert.True(t, ctx.Written())
|
|
|
|
assert.Contains(t, resp.Body.String(), testCase.err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-08-25 15:51:26 +02:00
|
|
|
func Test_artifactsFind(t *testing.T) {
|
|
|
|
unittest.PrepareTestEnv(t)
|
|
|
|
|
|
|
|
for _, testCase := range []struct {
|
|
|
|
name string
|
|
|
|
artifactName string
|
|
|
|
count int
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "Found",
|
|
|
|
artifactName: "artifact-v4-download",
|
|
|
|
count: 1,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "NotFound",
|
|
|
|
artifactName: "notexist",
|
|
|
|
count: 0,
|
|
|
|
},
|
|
|
|
} {
|
|
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
|
|
runID := int64(792)
|
|
|
|
ctx, _ := contexttest.MockContext(t, fmt.Sprintf("user5/repo4/actions/runs/%v/artifacts/%v", runID, testCase.artifactName))
|
|
|
|
artifacts := artifactsFind(ctx, actions_model.FindArtifactsOptions{
|
|
|
|
RunID: runID,
|
|
|
|
ArtifactName: testCase.artifactName,
|
|
|
|
})
|
|
|
|
assert.False(t, ctx.Written())
|
|
|
|
assert.Len(t, artifacts, testCase.count)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func Test_artifactsFindByNameOrID(t *testing.T) {
|
|
|
|
unittest.PrepareTestEnv(t)
|
|
|
|
|
|
|
|
for _, testCase := range []struct {
|
|
|
|
name string
|
|
|
|
nameOrID string
|
|
|
|
err string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "NameFound",
|
|
|
|
nameOrID: "artifact-v4-download",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "NameNotFound",
|
|
|
|
nameOrID: "notexist",
|
|
|
|
err: "artifact name not found",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "IDFound",
|
|
|
|
nameOrID: "22",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "IDNotFound",
|
|
|
|
nameOrID: "666",
|
|
|
|
err: "artifact ID not found",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "IDZeroNotFound",
|
|
|
|
nameOrID: "0",
|
|
|
|
err: "artifact name not found",
|
|
|
|
},
|
|
|
|
} {
|
|
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
|
|
runID := int64(792)
|
|
|
|
ctx, resp := contexttest.MockContext(t, fmt.Sprintf("user5/repo4/actions/runs/%v/artifacts/%v", runID, testCase.nameOrID))
|
|
|
|
artifacts := artifactsFindByNameOrID(ctx, runID, testCase.nameOrID)
|
|
|
|
if testCase.err == "" {
|
|
|
|
assert.NotEmpty(t, artifacts)
|
|
|
|
assert.False(t, ctx.Written(), resp.Body.String())
|
|
|
|
} else {
|
|
|
|
assert.Empty(t, artifacts)
|
|
|
|
assert.True(t, ctx.Written())
|
|
|
|
assert.Contains(t, resp.Body.String(), testCase.err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2025-08-26 21:29:22 -06:00
|
|
|
|
|
|
|
func baseExpectedResponse() *ViewResponse {
|
|
|
|
return &ViewResponse{
|
|
|
|
State: ViewState{
|
|
|
|
Run: ViewRunInfo{
|
|
|
|
Link: "/user5/repo4/actions/runs/187",
|
|
|
|
Title: "update actions",
|
|
|
|
TitleHTML: template.HTML("update actions"),
|
|
|
|
Status: "success",
|
|
|
|
CanCancel: false,
|
|
|
|
CanApprove: false,
|
|
|
|
CanRerun: false,
|
|
|
|
CanDeleteArtifact: false,
|
|
|
|
Done: true,
|
|
|
|
Jobs: []*ViewJob{
|
|
|
|
{
|
|
|
|
ID: 192,
|
|
|
|
Name: "job_2",
|
|
|
|
Status: "success",
|
|
|
|
CanRerun: false,
|
|
|
|
Duration: "1m38s",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Commit: ViewCommit{
|
|
|
|
LocaleCommit: "actions.runs.commit",
|
|
|
|
LocalePushedBy: "actions.runs.pushed_by",
|
|
|
|
LocaleWorkflow: "actions.runs.workflow",
|
|
|
|
ShortSha: "c2d72f5484",
|
|
|
|
Link: "/user5/repo4/commit/c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
|
|
|
Pusher: ViewUser{
|
|
|
|
DisplayName: "user1",
|
|
|
|
Link: "/user1",
|
|
|
|
},
|
|
|
|
Branch: ViewBranch{
|
|
|
|
Name: "master",
|
|
|
|
Link: "/user5/repo4/src/branch/master",
|
|
|
|
IsDeleted: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
CurrentJob: ViewCurrentJob{
|
|
|
|
Title: "job_2",
|
|
|
|
Detail: "actions.status.success",
|
|
|
|
Steps: []*ViewJobStep{
|
|
|
|
{
|
|
|
|
Summary: "Set up job",
|
|
|
|
Status: "running",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Summary: "Complete job",
|
|
|
|
Status: "waiting",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Logs: ViewLogs{
|
|
|
|
StepsLog: []*ViewStepLog{},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestActionsViewViewPost(t *testing.T) {
|
|
|
|
unittest.PrepareTestEnv(t)
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
runIndex int64
|
|
|
|
jobIndex int64
|
|
|
|
expected *ViewResponse
|
|
|
|
expectedTweaks func(*ViewResponse)
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "base case",
|
|
|
|
runIndex: 187,
|
|
|
|
jobIndex: 0,
|
|
|
|
expected: baseExpectedResponse(),
|
|
|
|
expectedTweaks: func(resp *ViewResponse) {
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "run with waiting jobs",
|
|
|
|
runIndex: 189,
|
|
|
|
jobIndex: 0,
|
|
|
|
expected: baseExpectedResponse(),
|
|
|
|
expectedTweaks: func(resp *ViewResponse) {
|
|
|
|
// Variations from runIndex 187 -> runIndex 189 that are not the subject of this test...
|
|
|
|
resp.State.Run.Link = "/user5/repo4/actions/runs/189"
|
|
|
|
resp.State.Run.Title = "job output"
|
|
|
|
resp.State.Run.TitleHTML = "job output"
|
|
|
|
resp.State.Run.Jobs = []*ViewJob{
|
|
|
|
{
|
|
|
|
ID: 194,
|
|
|
|
Name: "job1 (1)",
|
|
|
|
Status: "success",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ID: 195,
|
|
|
|
Name: "job1 (2)",
|
|
|
|
Status: "success",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
ID: 196,
|
|
|
|
Name: "job2",
|
|
|
|
Status: "waiting",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
resp.State.CurrentJob.Title = "job1 (1)"
|
|
|
|
resp.State.CurrentJob.Steps = []*ViewJobStep{
|
|
|
|
{
|
|
|
|
Summary: "Set up job",
|
|
|
|
Status: "success",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Summary: "Complete job",
|
|
|
|
Status: "success",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// Under test in this case: verify that Done is set to false; in the fixture data, job.ID=195 is status
|
|
|
|
// Success, but job.ID=196 is status Waiting, and so we expect to signal Done=false to indicate to the
|
|
|
|
// UI to continue refreshing the page.
|
|
|
|
resp.State.Run.Done = false
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
ctx, resp := contexttest.MockContext(t, "user2/repo1/actions/runs/0")
|
|
|
|
contexttest.LoadUser(t, ctx, 2)
|
|
|
|
contexttest.LoadRepo(t, ctx, 4)
|
|
|
|
ctx.SetParams(":run", fmt.Sprintf("%d", tt.runIndex))
|
|
|
|
ctx.SetParams(":job", fmt.Sprintf("%d", tt.jobIndex))
|
|
|
|
web.SetForm(ctx, &ViewRequest{})
|
|
|
|
|
|
|
|
ViewPost(ctx)
|
|
|
|
require.Equal(t, http.StatusOK, resp.Result().StatusCode)
|
|
|
|
|
|
|
|
var actual ViewResponse
|
|
|
|
err := json.Unmarshal(resp.Body.Bytes(), &actual)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// `Duration` field is dynamic based upon current time, so eliminate it from comparison -- but check that it
|
|
|
|
// has the right format at least.
|
|
|
|
zeroDurations := func(vr *ViewResponse) {
|
|
|
|
for _, job := range vr.State.Run.Jobs {
|
|
|
|
assert.Regexp(t, `^(\d+[hms]){1,3}$`, job.Duration)
|
|
|
|
job.Duration = ""
|
|
|
|
}
|
|
|
|
for _, step := range vr.State.CurrentJob.Steps {
|
|
|
|
step.Duration = ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
zeroDurations(&actual)
|
|
|
|
zeroDurations(tt.expected)
|
|
|
|
tt.expectedTweaks(tt.expected)
|
|
|
|
|
|
|
|
assert.Equal(t, *tt.expected, actual)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|