Files
gh-action-readme/testutil/fixtures_test.go
Ismo Vuorinen 7f80105ff5 feat: go 1.25.5, dependency updates, renamed internal/errors (#129)
* feat: rename internal/errors to internal/apperrors

* fix(tests): clear env values before using in tests

* feat: rename internal/errors to internal/apperrors

* chore(deps): update go and all dependencies

* chore: remove renovate from pre-commit, formatting

* chore: sonarcloud fixes

* feat: consolidate constants to appconstants/constants.go

* chore: sonarcloud fixes

* feat: simplification, deduplication, test utils

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: clean up

* fix: config discovery, const deduplication

* chore: fixes
2026-01-01 23:17:29 +02:00

589 lines
14 KiB
Go

package testutil
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/goccy/go-yaml"
)
const testVersion = "v4.1.1"
func TestMustReadFixture(t *testing.T) {
t.Parallel()
tests := []struct {
name string
filename string
wantErr bool
}{
{
name: "valid fixture file",
filename: "simple-action.yml",
wantErr: false,
},
{
name: "another valid fixture",
filename: "composite-action.yml",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if tt.wantErr {
defer func() {
if r := recover(); r == nil {
t.Error("expected panic but got none")
}
}()
}
content := mustReadFixture(tt.filename)
if !tt.wantErr {
if content == "" {
t.Error("expected non-empty content")
}
// Verify it's valid YAML
var yamlContent map[string]any
if err := yaml.Unmarshal([]byte(content), &yamlContent); err != nil {
t.Errorf("fixture content is not valid YAML: %v", err)
}
}
})
}
}
func TestMustReadFixture_Panic(t *testing.T) {
t.Parallel()
t.Run("missing file panics", func(t *testing.T) {
t.Parallel()
ExpectPanic(t, func() {
mustReadFixture("nonexistent-file.yml")
}, "failed to read fixture")
})
}
func TestGitHubAPIResponses(t *testing.T) {
t.Parallel()
t.Run("GitHubReleaseResponse", func(t *testing.T) {
t.Parallel()
testGitHubReleaseResponse(t)
})
t.Run("GitHubTagResponse", func(t *testing.T) {
t.Parallel()
testGitHubTagResponse(t)
})
t.Run("GitHubRepoResponse", func(t *testing.T) {
t.Parallel()
testGitHubRepoResponse(t)
})
t.Run("GitHubCommitResponse", func(t *testing.T) {
t.Parallel()
testGitHubCommitResponse(t)
})
t.Run("GitHubRateLimitResponse", func(t *testing.T) {
t.Parallel()
testGitHubRateLimitResponse(t)
})
t.Run("GitHubErrorResponse", func(t *testing.T) {
t.Parallel()
testGitHubErrorResponse(t)
})
}
// testGitHubReleaseResponse validates the GitHub release response format.
func testGitHubReleaseResponse(t *testing.T) {
t.Helper()
data := parseJSONResponse(t, GitHubReleaseResponse)
if data["id"] == nil {
t.Error("expected id field")
}
if data["tag_name"] != testVersion {
t.Errorf("expected tag_name %s, got %v", testVersion, data["tag_name"])
}
if data["name"] != testVersion {
t.Errorf("expected name %s, got %v", testVersion, data["name"])
}
}
// testGitHubTagResponse validates the GitHub tag response format.
func testGitHubTagResponse(t *testing.T) {
t.Helper()
data := parseJSONResponse(t, GitHubTagResponse)
if data["name"] != testVersion {
t.Errorf("expected name %s, got %v", testVersion, data["name"])
}
if data["commit"] == nil {
t.Error("expected commit field")
}
}
// testGitHubRepoResponse validates the GitHub repository response format.
func testGitHubRepoResponse(t *testing.T) {
t.Helper()
data := parseJSONResponse(t, GitHubRepoResponse)
if data["name"] != "checkout" {
t.Errorf("expected name checkout, got %v", data["name"])
}
if data["full_name"] != "actions/checkout" {
t.Errorf("expected full_name actions/checkout, got %v", data["full_name"])
}
}
// testGitHubCommitResponse validates the GitHub commit response format.
func testGitHubCommitResponse(t *testing.T) {
t.Helper()
data := parseJSONResponse(t, GitHubCommitResponse)
if data["sha"] == nil {
t.Error("expected sha field")
}
if data["commit"] == nil {
t.Error("expected commit field")
}
}
// testGitHubRateLimitResponse validates the GitHub rate limit response format.
func testGitHubRateLimitResponse(t *testing.T) {
t.Helper()
data := parseJSONResponse(t, GitHubRateLimitResponse)
if data["resources"] == nil {
t.Error("expected resources field")
}
if data["rate"] == nil {
t.Error("expected rate field")
}
}
// testGitHubErrorResponse validates the GitHub error response format.
func testGitHubErrorResponse(t *testing.T) {
t.Helper()
data := parseJSONResponse(t, GitHubErrorResponse)
if data["message"] != "Not Found" {
t.Errorf("expected message 'Not Found', got %v", data["message"])
}
}
// parseJSONResponse parses a JSON response string and returns the data map.
func parseJSONResponse(t *testing.T, response string) map[string]any {
t.Helper()
var data map[string]any
if err := json.Unmarshal([]byte(response), &data); err != nil {
t.Fatalf("failed to parse JSON response: %v", err)
}
return data
}
func TestSimpleTemplate(t *testing.T) {
t.Parallel()
template := SimpleTemplate
// Check that template contains expected sections
expectedSections := []string{
"# {{ .Name }}",
"{{ .Description }}",
"## Installation",
"uses: {{ gitOrg . }}/{{ gitRepo . }}@{{ actionVersion . }}",
"## Inputs",
"## Outputs",
}
for _, section := range expectedSections {
if !strings.Contains(template, section) {
t.Errorf("template missing expected section: %s", section)
}
}
// Verify template has proper structure
if !strings.Contains(template, "```yaml") {
t.Error("template should contain YAML code blocks")
}
if !strings.Contains(template, "| Name | Description |") {
t.Error("template should contain table headers")
}
}
func TestMockGitHubResponses(t *testing.T) {
t.Parallel()
responses := MockGitHubResponses()
// Test that all expected endpoints are present
expectedEndpoints := []string{
"GET https://api.github.com/repos/actions/checkout/releases/latest",
"GET https://api.github.com/repos/actions/checkout/git/ref/tags/v4.1.1",
"GET https://api.github.com/repos/actions/checkout/tags",
"GET https://api.github.com/repos/actions/checkout",
"GET https://api.github.com/rate_limit",
"GET https://api.github.com/repos/actions/setup-node/releases/latest",
}
for _, endpoint := range expectedEndpoints {
if _, exists := responses[endpoint]; !exists {
t.Errorf("missing endpoint: %s", endpoint)
}
}
// Test that all responses are valid JSON
for endpoint, response := range responses {
var data any
if err := json.Unmarshal([]byte(response), &data); err != nil {
t.Errorf("invalid JSON for endpoint %s: %v", endpoint, err)
}
}
// Test specific response structures
t.Run("checkout releases response", func(t *testing.T) {
t.Parallel()
response := responses["GET https://api.github.com/repos/actions/checkout/releases/latest"]
var release map[string]any
if err := json.Unmarshal([]byte(response), &release); err != nil {
t.Fatalf("failed to parse release response: %v", err)
}
if release["tag_name"] == nil {
t.Error("release response missing tag_name")
}
})
}
func TestFixtureConstants(t *testing.T) {
t.Parallel()
// Test that all fixture variables are properly loaded
fixtures := map[string]string{
"SimpleActionYML": MustReadFixture("actions/javascript/simple.yml"),
"CompositeActionYML": MustReadFixture("actions/composite/basic.yml"),
"DockerActionYML": MustReadFixture("actions/docker/basic.yml"),
"InvalidActionYML": MustReadFixture("actions/invalid/missing-description.yml"),
"MinimalActionYML": MustReadFixture("minimal-action.yml"),
"TestProjectActionYML": MustReadFixture("test-project-action.yml"),
"RepoSpecificConfigYAML": MustReadFixture("repo-config.yml"),
"PackageJSONContent": PackageJSONContent,
}
for name, content := range fixtures {
t.Run(name, func(t *testing.T) {
t.Parallel()
if content == "" {
t.Errorf("%s is empty", name)
}
// For YAML fixtures, verify they're valid YAML (except InvalidActionYML)
if strings.HasSuffix(name, "YML") || strings.HasSuffix(name, "YAML") {
if name != "InvalidActionYML" {
var yamlContent map[string]any
if err := yaml.Unmarshal([]byte(content), &yamlContent); err != nil {
t.Errorf("%s contains invalid YAML: %v", name, err)
}
}
}
// For JSON fixtures, verify they're valid JSON
if strings.Contains(name, "JSON") {
var jsonContent any
if err := json.Unmarshal([]byte(content), &jsonContent); err != nil {
t.Errorf("%s contains invalid JSON: %v", name, err)
}
}
})
}
}
func TestGitIgnoreContent(t *testing.T) {
t.Parallel()
content := GitIgnoreContent
expectedPatterns := []string{
"node_modules/",
"*.log",
"dist/",
"build/",
".DS_Store",
"Thumbs.db",
}
for _, pattern := range expectedPatterns {
if !strings.Contains(content, pattern) {
t.Errorf("GitIgnoreContent missing pattern: %s", pattern)
}
}
// Verify it has comments
if !strings.Contains(content, "# Dependencies") {
t.Error("GitIgnoreContent should contain section comments")
}
}
// Test helper functions that interact with the filesystem.
func TestFixtureFileSystem(t *testing.T) {
t.Parallel()
// Verify that the fixture files actually exist
fixtureFiles := []string{
"simple-action.yml",
"composite-action.yml",
"docker-action.yml",
"invalid-action.yml",
"minimal-action.yml",
"test-project-action.yml",
"repo-config.yml",
"package.json",
"dynamic-action-template.yml",
"composite-template.yml",
}
// Get the testdata directory path
projectRoot := func() string {
wd, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get working directory: %v", err)
}
return filepath.Dir(wd) // Go up from testutil to project root
}()
fixturesDir := filepath.Join(projectRoot, "testdata", "yaml-fixtures")
for _, filename := range fixtureFiles {
t.Run(filename, func(t *testing.T) {
t.Parallel()
path := filepath.Join(fixturesDir, filename)
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Errorf("fixture file does not exist: %s", path)
}
})
}
}
// Tests for FixtureManager functionality (consolidated from scenarios.go tests)
func TestNewFixtureManager(t *testing.T) {
t.Parallel()
fm := NewFixtureManager()
if fm == nil {
t.Fatal("expected fixture manager to be created")
}
if fm.basePath == "" {
t.Error("expected basePath to be set")
}
if fm.scenarios == nil {
t.Error("expected scenarios map to be initialized")
}
if fm.cache == nil {
t.Error("expected cache map to be initialized")
}
}
func TestFixtureManagerLoadScenarios(t *testing.T) {
t.Parallel()
fm := NewFixtureManager()
// Test loading scenarios (will create default if none exist)
err := fm.LoadScenarios()
if err != nil {
t.Fatalf("failed to load scenarios: %v", err)
}
// Should have some default scenarios
if len(fm.scenarios) == 0 {
t.Error("expected default scenarios to be loaded")
}
}
func TestFixtureManagerActionTypes(t *testing.T) {
t.Parallel()
fm := NewFixtureManager()
tests := []struct {
name string
content string
expected ActionType
}{
{
name: "javascript action",
content: "using: 'node20'",
expected: ActionTypeJavaScript,
},
{
name: "composite action",
content: "using: 'composite'",
expected: ActionTypeComposite,
},
{
name: "docker action",
content: "using: 'docker'",
expected: ActionTypeDocker,
},
{
name: "minimal action",
content: "name: test",
expected: ActionTypeMinimal,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actualType := fm.determineActionTypeByContent(tt.content)
if actualType != tt.expected {
t.Errorf("expected action type %s, got %s", tt.expected, actualType)
}
})
}
}
func TestFixtureManagerValidation(t *testing.T) {
t.Parallel()
fm := NewFixtureManager()
tests := []struct {
name string
fixture string
expected bool
}{
{
name: "valid action",
fixture: "validation/valid-action.yml",
expected: true,
},
{
name: "missing name",
fixture: "validation/missing-name.yml",
expected: false,
},
{
name: "missing description",
fixture: "validation/missing-description.yml",
expected: false,
},
{
name: "missing runs",
fixture: "validation/missing-runs.yml",
expected: false,
},
{
name: "invalid yaml",
fixture: "validation/invalid-yaml.yml",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
content := MustReadFixture(tt.fixture)
isValid := fm.validateFixtureContent(content)
if isValid != tt.expected {
t.Errorf("expected validation result %v, got %v", tt.expected, isValid)
}
})
}
}
func TestGetFixtureManager(t *testing.T) {
t.Parallel()
// Test singleton behavior
fm1 := GetFixtureManager()
fm2 := GetFixtureManager()
if fm1 != fm2 {
t.Error("expected GetFixtureManager to return same instance")
}
if fm1 == nil {
t.Fatal("expected fixture manager to be created")
}
}
func TestActionFixtureLoading(t *testing.T) {
t.Parallel()
// Test loading a fixture that should exist
fixture, err := LoadActionFixture("simple-action.yml")
if err != nil {
t.Fatalf("failed to load simple action fixture: %v", err)
}
if fixture == nil {
t.Fatal("expected fixture to be loaded")
}
if fixture.Name == "" {
t.Error("expected fixture name to be set")
}
if fixture.Content == "" {
t.Error("expected fixture content to be loaded")
}
if fixture.ActionType == "" {
t.Error("expected action type to be determined")
}
}
// Test helper functions for other components
func TestHelperFunctions(t *testing.T) {
t.Parallel()
t.Run("GetValidFixtures", func(t *testing.T) {
t.Parallel()
validFixtures := GetValidFixtures()
if len(validFixtures) == 0 {
t.Skip("no valid fixtures available")
}
for _, fixture := range validFixtures {
if fixture == "" {
t.Error("fixture name should not be empty")
}
}
})
t.Run("GetInvalidFixtures", func(t *testing.T) {
t.Parallel()
invalidFixtures := GetInvalidFixtures()
// It's okay if there are no invalid fixtures for testing
for _, fixture := range invalidFixtures {
if fixture == "" {
t.Error("fixture name should not be empty")
}
}
})
t.Run("GetFixturesByActionType", func(t *testing.T) {
t.Parallel()
javascriptFixtures := GetFixturesByActionType(ActionTypeJavaScript)
compositeFixtures := GetFixturesByActionType(ActionTypeComposite)
dockerFixtures := GetFixturesByActionType(ActionTypeDocker)
// We don't require specific fixtures to exist, just test the function works
_ = javascriptFixtures
_ = compositeFixtures
_ = dockerFixtures
})
t.Run("GetFixturesByTag", func(t *testing.T) {
t.Parallel()
validTaggedFixtures := GetFixturesByTag("valid")
invalidTaggedFixtures := GetFixturesByTag("invalid")
basicTaggedFixtures := GetFixturesByTag("basic")
// We don't require specific fixtures to exist, just test the function works
_ = validTaggedFixtures
_ = invalidTaggedFixtures
_ = basicTaggedFixtures
})
}