Files
gh-action-readme/internal/template_test.go
Ismo Vuorinen ce23f93b74 feat: detect permissions from actions (#137)
* feat: detect permissions from actions

* refactor(test): fix 25 SonarCloud issues by extracting test constants

Resolved all SonarCloud code quality issues for PR #137:
- Fixed 12 string duplication issues (S1192)
- Fixed 13 naming convention issues (S100)

Changes:
- Centralized test constants in appconstants/test_constants.go
  * Added 9 parser test constants for YAML templates
  * Added 3 template test constants for paths and versions
- Updated parser_test.go to use shared constants
- Updated template_test.go to use shared constants
- Renamed 13 test functions to camelCase (removed underscores)

* chore: reduce code duplication

* fix: implement cr fixes

* chore: deduplication
2026-01-04 02:48:29 +02:00

534 lines
12 KiB
Go

package internal
import (
"path/filepath"
"testing"
"github.com/ivuorinen/gh-action-readme/appconstants"
"github.com/ivuorinen/gh-action-readme/internal/git"
)
// newTemplateData creates a TemplateData with common test values.
// Pass nil for any field to use defaults or zero values.
func newTemplateData(
actionName string,
version string,
useDefaultBranch bool,
defaultBranch string,
org string,
repo string,
actionPath string,
repoRoot string,
) *TemplateData {
var actionYML *ActionYML
if actionName != "" {
actionYML = &ActionYML{Name: actionName}
}
return &TemplateData{
ActionYML: actionYML,
Config: &AppConfig{
Version: version,
UseDefaultBranch: useDefaultBranch,
},
Git: git.RepoInfo{
Organization: org,
Repository: repo,
DefaultBranch: defaultBranch,
},
ActionPath: actionPath,
RepoRoot: repoRoot,
}
}
// TestExtractActionSubdirectory tests the extractActionSubdirectory function.
func TestExtractActionSubdirectory(t *testing.T) {
t.Parallel()
tests := []struct {
name string
actionPath string
repoRoot string
want string
}{
{
name: "subdirectory action",
actionPath: "/repo/actions/csharp-build/action.yml",
repoRoot: "/repo",
want: "actions/csharp-build",
},
{
name: "single level subdirectory",
actionPath: appconstants.TestRepoBuildActionPath,
repoRoot: "/repo",
want: "build",
},
{
name: "deeply nested subdirectory",
actionPath: "/repo/a/b/c/d/action.yml",
repoRoot: "/repo",
want: "a/b/c/d",
},
{
name: "root action",
actionPath: appconstants.TestRepoActionPath,
repoRoot: "/repo",
want: "",
},
{
name: "empty action path",
actionPath: "",
repoRoot: "/repo",
want: "",
},
{
name: "empty repo root",
actionPath: appconstants.TestRepoActionPath,
repoRoot: "",
want: "",
},
{
name: "both empty",
actionPath: "",
repoRoot: "",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := extractActionSubdirectory(tt.actionPath, tt.repoRoot)
// Normalize paths for cross-platform compatibility
want := filepath.ToSlash(tt.want)
got = filepath.ToSlash(got)
if got != want {
t.Errorf("extractActionSubdirectory() = %q, want %q", got, want)
}
})
}
}
// TestBuildUsesString tests the buildUsesString function with subdirectory extraction.
func TestBuildUsesString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
td *TemplateData
org string
repo string
version string
want string
}{
{
name: "monorepo with subdirectory",
td: &TemplateData{
ActionPath: "/repo/actions/csharp-build/action.yml",
RepoRoot: "/repo",
},
org: "ivuorinen",
repo: "actions",
version: "@main",
want: "ivuorinen/actions/actions/csharp-build@main",
},
{
name: "root action",
td: &TemplateData{
ActionPath: appconstants.TestRepoActionPath,
RepoRoot: "/repo",
},
org: "ivuorinen",
repo: "my-action",
version: "@main",
want: "ivuorinen/my-action@main",
},
{
name: "empty org",
td: &TemplateData{
ActionPath: appconstants.TestRepoBuildActionPath,
RepoRoot: "/repo",
},
org: "",
repo: "actions",
version: "@main",
want: "your-org/your-action@v1",
},
{
name: "empty repo",
td: &TemplateData{
ActionPath: appconstants.TestRepoBuildActionPath,
RepoRoot: "/repo",
},
org: "ivuorinen",
repo: "",
version: "@main",
want: "your-org/your-action@v1",
},
{
name: "missing paths in template data",
td: &TemplateData{
ActionPath: "",
RepoRoot: "",
},
org: "ivuorinen",
repo: "actions",
version: "@v1",
want: "ivuorinen/actions@v1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := buildUsesString(tt.td, tt.org, tt.repo, tt.version)
// Normalize paths for cross-platform compatibility
want := filepath.ToSlash(tt.want)
got = filepath.ToSlash(got)
if got != want {
t.Errorf("buildUsesString() = %q, want %q", got, want)
}
})
}
}
// TestGetActionVersion tests the getActionVersion function with priority logic.
func TestGetActionVersion(t *testing.T) {
t.Parallel()
tests := []struct {
name string
data any
want string
}{
{
name: "config version override",
data: newTemplateData("", "v2.0.0", true, "main", "", "", "", ""),
want: "v2.0.0",
},
{
name: "use default branch when enabled",
data: newTemplateData("", "", true, "main", "", "", "", ""),
want: "main",
},
{
name: "use default branch master",
data: newTemplateData("", "", true, "master", "", "", "", ""),
want: "master",
},
{
name: "fallback to v1 when default branch disabled",
data: newTemplateData("", "", false, "main", "", "", "", ""),
want: "v1",
},
{
name: "fallback to v1 when default branch not detected",
data: newTemplateData("", "", true, "", "", "", "", ""),
want: "v1",
},
{
name: "fallback to v1 when data is invalid",
data: "invalid",
want: "v1",
},
{
name: "fallback to v1 when data is nil",
data: nil,
want: "v1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := getActionVersion(tt.data)
if got != tt.want {
t.Errorf("getActionVersion() = %q, want %q", got, tt.want)
}
})
}
}
// TestGetGitUsesString tests the complete integration of gitUsesString template function.
func TestGetGitUsesString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
data *TemplateData
want string
}{
{
name: "monorepo action with default branch",
data: newTemplateData("C# Build", "", true, "main", "ivuorinen", "actions",
"/repo/csharp-build/action.yml", "/repo"),
want: "ivuorinen/actions/csharp-build@main",
},
{
name: "monorepo action with explicit version",
data: newTemplateData("Build Action", "v1.0.0", true, "main", "org", "actions",
appconstants.TestRepoBuildActionPath, "/repo"),
want: "org/actions/build@v1.0.0",
},
{
name: "root level action with default branch",
data: newTemplateData("My Action", "", true, "develop", "user", "my-action",
appconstants.TestRepoActionPath, "/repo"),
want: "user/my-action@develop",
},
{
name: "action with use_default_branch disabled",
data: newTemplateData("Test Action", "", false, "main", "org", "test",
appconstants.TestRepoActionPath, "/repo"),
want: "org/test@v1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := getGitUsesString(tt.data)
// Normalize paths for cross-platform compatibility
want := filepath.ToSlash(tt.want)
got = filepath.ToSlash(got)
if got != want {
t.Errorf("getGitUsesString() = %q, want %q", got, want)
}
})
}
}
// TestFormatVersion tests the formatVersion function.
func TestFormatVersion(t *testing.T) {
t.Parallel()
tests := []struct {
name string
version string
want string
}{
{
name: "empty version",
version: "",
want: "@v1",
},
{
name: "whitespace only version",
version: " ",
want: "@v1",
},
{
name: "version without @",
version: "v1.2.3",
want: appconstants.TestVersionV123,
},
{
name: "version with @",
version: appconstants.TestVersionV123,
want: appconstants.TestVersionV123,
},
{
name: "main branch",
version: "main",
want: "@main",
},
{
name: "version with @ and spaces",
version: " @v2.0.0 ",
want: "@v2.0.0",
},
{
name: "sha version",
version: "abc123",
want: "@abc123",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := formatVersion(tt.version)
if got != tt.want {
t.Errorf("formatVersion(%q) = %q, want %q", tt.version, got, tt.want)
}
})
}
}
// TestBuildTemplateData tests the BuildTemplateData function.
func TestBuildTemplateData(t *testing.T) {
t.Parallel()
tests := []struct {
name string
action *ActionYML
config *AppConfig
repoRoot string
actionPath string
wantOrg string
wantRepo string
}{
{
name: "basic action with config overrides",
action: &ActionYML{
Name: "Test Action",
Description: "Test description",
},
config: &AppConfig{
Organization: "testorg",
Repository: "testrepo",
},
repoRoot: ".",
actionPath: "action.yml",
wantOrg: "testorg",
wantRepo: "testrepo",
},
{
name: "action without config overrides",
action: &ActionYML{
Name: "Another Action",
Description: "Another description",
},
config: &AppConfig{},
repoRoot: ".",
actionPath: "action.yml",
wantOrg: "",
wantRepo: "",
},
{
name: "action with dependency analysis enabled",
action: &ActionYML{
Name: "Dep Action",
Description: "Action with deps",
},
config: &AppConfig{
Organization: "deporg",
Repository: "deprepo",
AnalyzeDependencies: true,
},
repoRoot: ".",
actionPath: "../testdata/composite-action/action.yml",
wantOrg: "deporg",
wantRepo: "deprepo",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
data := BuildTemplateData(tt.action, tt.config, tt.repoRoot, tt.actionPath)
assertTemplateData(t, data, tt.action, tt.config, tt.wantOrg, tt.wantRepo)
})
}
}
func assertTemplateData(
t *testing.T,
data *TemplateData,
action *ActionYML,
config *AppConfig,
wantOrg, wantRepo string,
) {
t.Helper()
if data == nil {
t.Fatal("BuildTemplateData() returned nil")
}
if data.ActionYML != action {
t.Error("BuildTemplateData() did not preserve ActionYML")
}
if data.Config != config {
t.Error("BuildTemplateData() did not preserve Config")
}
if config.Organization != "" && data.Git.Organization != wantOrg {
t.Errorf("BuildTemplateData() Git.Organization = %q, want %q", data.Git.Organization, wantOrg)
}
if config.Repository != "" && data.Git.Repository != wantRepo {
t.Errorf("BuildTemplateData() Git.Repository = %q, want %q", data.Git.Repository, wantRepo)
}
if config.AnalyzeDependencies && data.Dependencies == nil {
t.Error("BuildTemplateData() expected Dependencies to be set when AnalyzeDependencies is true")
}
}
// TestAnalyzeDependencies tests the analyzeDependencies function.
func TestAnalyzeDependencies(t *testing.T) {
t.Parallel()
tests := []struct {
name string
actionPath string
config *AppConfig
expectNil bool
}{
{
name: "valid composite action without GitHub token",
actionPath: "../../testdata/analyzer/composite-action.yml",
config: &AppConfig{},
expectNil: false,
},
{
name: "nonexistent action file",
actionPath: "../../testdata/analyzer/nonexistent.yml",
config: &AppConfig{},
expectNil: false, // Should return empty slice, not nil
},
{
name: "docker action without token",
actionPath: "../../testdata/analyzer/docker-action.yml",
config: &AppConfig{},
expectNil: false,
},
{
name: "javascript action without token",
actionPath: "../../testdata/analyzer/javascript-action.yml",
config: &AppConfig{},
expectNil: false,
},
{
name: "invalid yaml file",
actionPath: "../../testdata/analyzer/invalid.yml",
config: &AppConfig{},
expectNil: false, // Should gracefully handle errors and return empty slice
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
gitInfo := git.RepoInfo{
Organization: "testorg",
Repository: "testrepo",
}
result := analyzeDependencies(tt.actionPath, tt.config, gitInfo)
if tt.expectNil && result != nil {
t.Errorf("analyzeDependencies() expected nil, got %v", result)
}
if !tt.expectNil && result == nil {
t.Error("analyzeDependencies() returned nil, expected non-nil slice")
}
})
}
}