// Copyright 2025 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: GPL-3.0-or-later package actions import ( "os" "path/filepath" "strings" "testing" "forgejo.org/models/unittest" "forgejo.org/modules/git" "forgejo.org/modules/setting" "code.forgejo.org/forgejo/runner/v12/act/jobparser" "code.forgejo.org/forgejo/runner/v12/act/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.yaml.in/yaml/v3" ) const testWorkflow string = `on: workflow_call: inputs: example-string-required: required: true type: string name: test jobs: job1: name: "job1 (local)" runs-on: ubuntu-slim steps: - name: Echo inputs run: | echo example-string-required="${{ inputs.example-string-required }}" ` func TestExpandForJob(t *testing.T) { job := jobparser.Job{} err := yaml.Unmarshal([]byte("{ name: job1 }"), &job) require.NoError(t, err) assert.True(t, expandForJob(&job)) err = yaml.Unmarshal([]byte("{ name: job1, runs-on: ubuntu-latest }"), &job) require.NoError(t, err) assert.False(t, expandForJob(&job)) err = yaml.Unmarshal([]byte("{ name: job1, runs-on: [x64, ubuntu-latest] }"), &job) require.NoError(t, err) assert.False(t, expandForJob(&job)) } func TestExpandLocalReusableWorkflows(t *testing.T) { gitRepo, err := git.OpenRepository(git.DefaultContext, "./TestExpandLocalReusableWorkflows") require.NoError(t, err) defer gitRepo.Close() commit, err := gitRepo.GetCommit("e3868ecb4f8b483fc0bdd422561bf0062a7df907") require.NoError(t, err) fetcher := expandLocalReusableWorkflows(commit) require.NotNil(t, fetcher) t.Run("successful fetch", func(t *testing.T) { content, err := fetcher(&jobparser.Job{}, "./.forgejo/workflows/reusable-1.yml") require.NoError(t, err) assert.Equal(t, testWorkflow, string(content)) }) t.Run("file not exist", func(t *testing.T) { _, err = fetcher(&jobparser.Job{}, "./forgejo/workflows/reusable-2.yml") require.ErrorContains(t, err, "expanding reusable workflow failed to access path ./forgejo/workflows/reusable-2.yml: object does not exist") }) t.Run("do not expand due to runs-on", func(t *testing.T) { jobWithRunsOn := jobparser.Job{} err = yaml.Unmarshal([]byte("{ name: job1, runs-on: ubuntu-latest }"), &jobWithRunsOn) require.NoError(t, err) assert.False(t, expandForJob(&jobWithRunsOn)) _, err = fetcher(&jobWithRunsOn, "./.forgejo/workflows/reusable-1.yml") require.ErrorIs(t, jobparser.ErrUnsupportedReusableWorkflowFetch, err) }) } func replaceTestRepo(t *testing.T, owner, repo, replacement string) { t.Helper() // Copy the repository into the target path that `gitrepo.OpenRepository` will look for it. repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(owner), strings.ToLower(repo)+".git") err := os.RemoveAll(repoPath) // there's a default repo copied here by the fixture setup that we want to replace require.NoError(t, err) err = os.CopyFS(repoPath, os.DirFS(replacement)) require.NoError(t, err) } func TestLazyRepoExpandLocalReusableWorkflows(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) // Shouldn't need valid content if we never call the lazy evaluator lazy1, cleanup := lazyRepoExpandLocalReusableWorkflow(t.Context(), -123456, "this is not a valid commit SHA") assert.NotNil(t, lazy1) assert.NotNil(t, cleanup) cleanup() replaceTestRepo(t, "user2", "repo1", "./TestExpandLocalReusableWorkflows") lazy2, cleanup := lazyRepoExpandLocalReusableWorkflow(t.Context(), 1, "e3868ecb4f8b483fc0bdd422561bf0062a7df907") assert.NotNil(t, lazy2) assert.NotNil(t, cleanup) content, err := lazy2(&jobparser.Job{}, "./.forgejo/workflows/reusable-1.yml") require.NoError(t, err) assert.Equal(t, testWorkflow, string(content)) cleanup() } func TestExpandInstanceReusableWorkflows(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) tests := []struct { name string ref *model.NonLocalReusableWorkflowReference errIs error errorContains string repo string hasRunsOn bool }{ { name: "hasRunsOn", hasRunsOn: true, ref: &model.NonLocalReusableWorkflowReference{}, errIs: jobparser.ErrUnsupportedReusableWorkflowFetch, }, { name: "non-existent owner", ref: &model.NonLocalReusableWorkflowReference{ Org: "owner-does-not-exist", }, errorContains: "owner-does-not-exist: user does not exist", }, { name: "non-public owner", ref: &model.NonLocalReusableWorkflowReference{ Org: "user33", }, errorContains: "user33: user does not exist", }, { name: "non-existent repo", ref: &model.NonLocalReusableWorkflowReference{ Org: "user2", Repo: "repo10000", }, errorContains: "repo10000: repo does not exist", }, { name: "non-public repo", ref: &model.NonLocalReusableWorkflowReference{ Org: "user2", Repo: "repo2", }, errorContains: "repo2: repo does not exist", }, { name: "public repo", ref: &model.NonLocalReusableWorkflowReference{ Org: "user2", Repo: "repo1", GitPlatform: "forgejo", Filename: "reusable-1.yml", Ref: "main", }, repo: "./TestExpandLocalReusableWorkflows", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.repo != "" { replaceTestRepo(t, tt.ref.Org, tt.ref.Repo, tt.repo) } job := jobparser.Job{} if tt.hasRunsOn { err := yaml.Unmarshal([]byte("{ name: job1, runs-on: ubuntu-latest }"), &job) require.NoError(t, err) } fetcher := expandInstanceReusableWorkflows(t.Context()) content, err := fetcher(&job, tt.ref) if tt.errIs != nil { require.ErrorIs(t, err, tt.errIs) } else if tt.errorContains != "" { require.ErrorContains(t, err, tt.errorContains) } else { require.NoError(t, err) assert.Equal(t, testWorkflow, string(content)) } }) } }