Files
gh-action-readme/testutil/fixtures.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

843 lines
24 KiB
Go

// Package testutil provides testing fixtures and fixture management for gh-action-readme.
package testutil
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/goccy/go-yaml"
"github.com/ivuorinen/gh-action-readme/appconstants"
)
// fixtureCache provides thread-safe caching of fixture content.
var fixtureCache = struct {
mu sync.RWMutex
cache map[string]string
}{
cache: make(map[string]string),
}
// MustReadFixture reads a YAML fixture file from testdata/yaml-fixtures.
func MustReadFixture(filename string) string {
return mustReadFixture(filename)
}
// mustReadFixture reads a YAML fixture file from testdata/yaml-fixtures with caching.
func mustReadFixture(filename string) string {
// Try to get from cache first (read lock)
fixtureCache.mu.RLock()
if content, exists := fixtureCache.cache[filename]; exists {
fixtureCache.mu.RUnlock()
return content
}
fixtureCache.mu.RUnlock()
// Not in cache, acquire write lock and read from disk
fixtureCache.mu.Lock()
defer fixtureCache.mu.Unlock()
// Double-check in case another goroutine loaded it while we were waiting
if content, exists := fixtureCache.cache[filename]; exists {
return content
}
// Load from disk
_, currentFile, _, ok := runtime.Caller(0)
if !ok {
panic(appconstants.ErrFailedToGetCurrentFilePath)
}
// Get the project root (go up from testutil/fixtures.go to project root)
projectRoot := filepath.Dir(filepath.Dir(currentFile))
fixturePath := filepath.Join(projectRoot, appconstants.DirTestdata, appconstants.DirYAMLFixtures, filename)
contentBytes, err := os.ReadFile(fixturePath) // #nosec G304 -- test fixture path from project structure
if err != nil {
panic("failed to read fixture " + filename + ": " + err.Error())
}
content := string(contentBytes)
// Store in cache
fixtureCache.cache[filename] = content
return content
}
// ActionType represents the type of GitHub Action being tested.
type ActionType string
const (
// ActionTypeJavaScript represents JavaScript-based GitHub Actions that run on Node.js.
ActionTypeJavaScript ActionType = ActionType(appconstants.ActionTypeJavaScript)
// ActionTypeComposite represents composite GitHub Actions that combine multiple steps.
ActionTypeComposite ActionType = ActionType(appconstants.ActionTypeComposite)
// ActionTypeDocker represents Docker-based GitHub Actions that run in containers.
ActionTypeDocker ActionType = ActionType(appconstants.ActionTypeDocker)
// ActionTypeInvalid represents invalid or malformed GitHub Actions for testing error scenarios.
ActionTypeInvalid ActionType = ActionType(appconstants.ActionTypeInvalid)
// ActionTypeMinimal represents minimal GitHub Actions with basic configuration.
ActionTypeMinimal ActionType = ActionType(appconstants.ActionTypeMinimal)
)
// TestScenario represents a structured test scenario with metadata.
type TestScenario struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Description string `yaml:"description"`
ActionType ActionType `yaml:"action_type"`
Fixture string `yaml:"fixture"`
ExpectValid bool `yaml:"expect_valid"`
ExpectError bool `yaml:"expect_error"`
Tags []string `yaml:"tags"`
Metadata map[string]any `yaml:"metadata,omitempty"`
}
// ActionFixture represents a loaded action YAML fixture with metadata.
type ActionFixture struct {
Name string
Path string
Content string
ActionType ActionType
IsValid bool
Scenario *TestScenario
}
// ConfigFixture represents a loaded configuration YAML fixture.
type ConfigFixture struct {
Name string
Path string
Content string
Type string
IsValid bool
}
// FixtureManager manages test fixtures and scenarios.
type FixtureManager struct {
basePath string
scenarios map[string]*TestScenario
cache map[string]*ActionFixture
mu sync.RWMutex // protects cache map
}
// GitHub API response fixtures for testing.
// GitHubReleaseResponse is a mock GitHub release API response.
const GitHubReleaseResponse = `{
"id": 123456,
"tag_name": "v4.1.1",
"name": "v4.1.1",
"body": "## What's Changed\n* Fix checkout bug\n* Improve performance",
"draft": false,
"prerelease": false,
"created_at": "2023-11-01T10:00:00Z",
"published_at": "2023-11-01T10:00:00Z",
"tarball_url": "https://api.github.com/repos/actions/checkout/tarball/v4.1.1",
"zipball_url": "https://api.github.com/repos/actions/checkout/zipball/v4.1.1"
}`
// GitHubTagResponse is a mock GitHub tag API response.
const GitHubTagResponse = `{
"name": "v4.1.1",
"zipball_url": "https://github.com/actions/checkout/zipball/v4.1.1",
"tarball_url": "https://github.com/actions/checkout/tarball/v4.1.1",
"commit": {
"sha": "8f4b7f84bd579b95d7f0b90f8d8b6e5d9b8a7f6e",
"url": "https://api.github.com/repos/actions/checkout/commits/8f4b7f84bd579b95d7f0b90f8d8b6e5d9b8a7f6e"
},
"node_id": "REF_kwDOAJy2KM9yZXJlZnMvdGFncy92NC4xLjE"
}`
// GitHubRepoResponse is a mock GitHub repository API response.
const GitHubRepoResponse = `{
"id": 216219028,
"name": "checkout",
"full_name": "actions/checkout",
"description": "Action for checking out a repo",
"private": false,
"html_url": "https://github.com/actions/checkout",
"clone_url": "https://github.com/actions/checkout.git",
"git_url": "git://github.com/actions/checkout.git",
"ssh_url": "git@github.com:actions/checkout.git",
"default_branch": "main",
"created_at": "2019-10-16T19:40:57Z",
"updated_at": "2023-11-01T10:00:00Z",
"pushed_at": "2023-11-01T09:30:00Z",
"stargazers_count": 4521,
"watchers_count": 4521,
"forks_count": 1234,
"open_issues_count": 42,
"topics": ["github-actions", "checkout", "git"]
}`
// GitHubCommitResponse is a mock GitHub commit API response.
const GitHubCommitResponse = `{
"sha": "8f4b7f84bd579b95d7f0b90f8d8b6e5d9b8a7f6e",
"node_id": "C_kwDOAJy2KNoAKDhmNGI3Zjg0YmQ1NzliOTVkN2YwYjkwZjhkOGI2ZTVkOWI4YTdmNmU",
"commit": {
"message": "Fix checkout bug and improve performance",
"author": {
"name": "GitHub Actions",
"email": "actions@github.com",
"date": "2023-11-01T09:30:00Z"
},
"committer": {
"name": "GitHub Actions",
"email": "actions@github.com",
"date": "2023-11-01T09:30:00Z"
}
},
"html_url": "https://github.com/actions/checkout/commit/8f4b7f84bd579b95d7f0b90f8d8b6e5d9b8a7f6e"
}`
// GitHubRateLimitResponse is a mock GitHub rate limit API response.
const GitHubRateLimitResponse = `{
"resources": {
"core": {
"limit": 5000,
"used": 1,
"remaining": 4999,
"reset": 1699027200
},
"search": {
"limit": 30,
"used": 0,
"remaining": 30,
"reset": 1699027200
}
},
"rate": {
"limit": 5000,
"used": 1,
"remaining": 4999,
"reset": 1699027200
}
}`
// SimpleTemplate is a basic template for testing.
const SimpleTemplate = `# {{ .Name }}
{{ .Description }}
## Installation
` + "```yaml" + `
uses: {{ gitOrg . }}/{{ gitRepo . }}@{{ actionVersion . }}
` + "```" + `
{{ if .Inputs }}
## Inputs
| Name | Description | Required | Default |
|------|-------------|----------|---------|
{{ range $key, $input := .Inputs -}}
| ` + "`{{ $key }}`" + ` | {{ $input.Description }} | {{ $input.Required }} | {{ $input.Default }} |
{{ end -}}
{{ end }}
{{ if .Outputs }}
## Outputs
| Name | Description |
|------|-------------|
{{ range $key, $output := .Outputs -}}
| ` + "`{{ $key }}`" + ` | {{ $output.Description }} |
{{ end -}}
{{ end }}
`
// GitHubErrorResponse is a mock GitHub error API response.
const GitHubErrorResponse = `{
"message": "Not Found",
"documentation_url": "https://docs.github.com/rest"
}`
// MockGitHubResponses returns a map of URL patterns to mock responses.
func MockGitHubResponses() map[string]string {
return map[string]string{
"GET https://api.github.com/repos/actions/checkout/releases/latest": GitHubReleaseResponse,
"GET https://api.github.com/repos/actions/checkout/git/ref/tags/v4.1.1": `{
"ref": "refs/tags/v4.1.1",
"node_id": "REF_kwDOAJy2KM9yZXJlZnMvdGFncy92NC4xLjE",
"url": "https://api.github.com/repos/actions/checkout/git/refs/tags/v4.1.1",
"object": {
"sha": "8f4b7f84bd579b95d7f0b90f8d8b6e5d9b8a7f6e",
"type": "commit",
"url": "https://api.github.com/repos/actions/checkout/git/commits/8f4b7f84bd579b95d7f0b90f8d8b6e5d9b8a7f6e"
}
}`,
"GET https://api.github.com/repos/actions/checkout/tags": `[` + GitHubTagResponse + `]`,
"GET https://api.github.com/repos/actions/checkout": GitHubRepoResponse,
"GET https://api.github.com/repos/actions/checkout/commits/" +
"8f4b7f84bd579b95d7f0b90f8d8b6e5d9b8a7f6e": GitHubCommitResponse,
"GET https://api.github.com/rate_limit": GitHubRateLimitResponse,
"GET https://api.github.com/repos/actions/setup-node/releases/latest": `{
"id": 123457,
"tag_name": "v4.0.0",
"name": "v4.0.0",
"body": "## What's Changed\n* Update Node.js versions\n* Fix compatibility issues",
"draft": false,
"prerelease": false,
"created_at": "2023-10-15T10:00:00Z",
"published_at": "2023-10-15T10:00:00Z"
}`,
"GET https://api.github.com/repos/actions/setup-node/git/ref/tags/v4.0.0": `{
"ref": "refs/tags/v4.0.0",
"node_id": "REF_kwDOAJy2KM9yZXJlZnMvdGFncy92NC4wLjA",
"url": "https://api.github.com/repos/actions/setup-node/git/refs/tags/v4.0.0",
"object": {
"sha": "1a4e6d7c9f8e5b2a3c4d5e6f7a8b9c0d1e2f3a4b",
"type": "commit",
"url": "https://api.github.com/repos/actions/setup-node/git/commits/1a4e6d7c9f8e5b2a3c4d5e6f7a8b9c0d1e2f3a4b"
}
}`,
"GET https://api.github.com/repos/actions/setup-node/tags": `[{
"name": "v4.0.0",
"commit": {
"sha": "1a4e6d7c9f8e5b2a3c4d5e6f7a8b9c0d1e2f3a4b",
"url": "https://api.github.com/repos/actions/setup-node/commits/1a4e6d7c9f8e5b2a3c4d5e6f7a8b9c0d1e2f3a4b"
}
}]`,
}
}
// GitIgnoreContent is a sample .gitignore file.
const GitIgnoreContent = `# Dependencies
node_modules/
*.log
# Build output
dist/
build/
# OS files
.DS_Store
Thumbs.db
`
// PackageJSONContent is a sample package.json file.
var PackageJSONContent = func() string {
var result string
result += "{\n"
result += " \"name\": \"test-action\",\n"
result += " \"version\": \"1.0.0\",\n"
result += " \"description\": \"Test GitHub Action\",\n"
result += " \"main\": \"index.js\",\n"
result += " \"scripts\": {\n"
result += " \"test\": \"jest\",\n"
result += " \"build\": \"webpack\"\n"
result += appconstants.JSONCloseBrace
result += " \"dependencies\": {\n"
result += " \"@actions/core\": \"^1.10.0\",\n"
result += " \"@actions/github\": \"^5.1.1\"\n"
result += appconstants.JSONCloseBrace
result += " \"devDependencies\": {\n"
result += " \"jest\": \"^29.0.0\",\n"
result += " \"webpack\": \"^5.0.0\"\n"
result += " }\n"
result += "}\n"
return result
}()
// NewFixtureManager creates a new fixture manager.
func NewFixtureManager() *FixtureManager {
_, currentFile, _, ok := runtime.Caller(0)
if !ok {
panic(appconstants.ErrFailedToGetCurrentFilePath)
}
// Get the project root (go up from testutil/fixtures.go to project root)
projectRoot := filepath.Dir(filepath.Dir(currentFile))
basePath := filepath.Join(projectRoot, appconstants.DirTestdata, appconstants.DirYAMLFixtures)
return &FixtureManager{
basePath: basePath,
scenarios: make(map[string]*TestScenario),
cache: make(map[string]*ActionFixture),
}
}
// LoadScenarios loads test scenarios from the scenarios directory.
func (fm *FixtureManager) LoadScenarios() error {
scenarioFile := filepath.Join(fm.basePath, "scenarios", "test-scenarios.yml")
// Create default scenarios if file doesn't exist
if _, err := os.Stat(scenarioFile); os.IsNotExist(err) {
return fm.createDefaultScenarios(scenarioFile)
}
data, err := os.ReadFile(scenarioFile) // #nosec G304 -- test fixture path from project structure
if err != nil {
return fmt.Errorf("failed to read scenarios file: %w", err)
}
var scenarios struct {
Scenarios []TestScenario `yaml:"scenarios"`
}
if err := yaml.Unmarshal(data, &scenarios); err != nil {
return fmt.Errorf("failed to parse scenarios YAML: %w", err)
}
for i := range scenarios.Scenarios {
scenario := &scenarios.Scenarios[i]
fm.scenarios[scenario.ID] = scenario
}
return nil
}
// LoadActionFixture loads an action fixture with metadata.
func (fm *FixtureManager) LoadActionFixture(name string) (*ActionFixture, error) {
// Check cache first with read lock
fm.mu.RLock()
if fixture, exists := fm.cache[name]; exists {
fm.mu.RUnlock()
return fixture, nil
}
fm.mu.RUnlock()
// Determine fixture path based on naming convention
fixturePath := fm.resolveFixturePath(name)
content, err := os.ReadFile(fixturePath) // #nosec G304 -- test fixture path resolution
if err != nil {
return nil, fmt.Errorf("failed to read fixture %s: %w", name, err)
}
fixture := &ActionFixture{
Name: name,
Path: fixturePath,
Content: string(content),
ActionType: fm.determineActionType(name, string(content)),
IsValid: fm.validateFixtureContent(string(content)),
}
// Try to find associated scenario
if scenario, exists := fm.scenarios[name]; exists {
fixture.Scenario = scenario
}
// Cache the fixture with write lock
fm.mu.Lock()
// Double-check cache in case another goroutine cached it while we were loading
if cachedFixture, exists := fm.cache[name]; exists {
fm.mu.Unlock()
return cachedFixture, nil
}
fm.cache[name] = fixture
fm.mu.Unlock()
return fixture, nil
}
// LoadConfigFixture loads a configuration fixture.
func (fm *FixtureManager) LoadConfigFixture(name string) (*ConfigFixture, error) {
configPath := filepath.Join(fm.basePath, "configs", name)
hasYMLExt := strings.HasSuffix(configPath, appconstants.ActionFileExtYML)
hasYAMLExt := strings.HasSuffix(configPath, appconstants.ActionFileExtYAML)
if !hasYMLExt && !hasYAMLExt {
configPath += appconstants.ActionFileExtYML
}
content, err := os.ReadFile(configPath) // #nosec G304 -- test fixture path from project structure
if err != nil {
return nil, fmt.Errorf("failed to read config fixture %s: %w", name, err)
}
return &ConfigFixture{
Name: name,
Path: configPath,
Content: string(content),
Type: fm.determineConfigType(name),
IsValid: fm.validateConfigContent(string(content)),
}, nil
}
// GetFixturesByTag returns fixture names matching the specified tags.
func (fm *FixtureManager) GetFixturesByTag(tags ...string) []string {
var matches []string
for _, scenario := range fm.scenarios {
if fm.scenarioMatchesTags(scenario, tags) {
matches = append(matches, scenario.Fixture)
}
}
return matches
}
// GetFixturesByActionType returns fixtures of a specific action type.
func (fm *FixtureManager) GetFixturesByActionType(actionType ActionType) []string {
var matches []string
for _, scenario := range fm.scenarios {
if scenario.ActionType == actionType {
matches = append(matches, scenario.Fixture)
}
}
return matches
}
// GetValidFixtures returns all fixtures that should parse as valid actions.
func (fm *FixtureManager) GetValidFixtures() []string {
var matches []string
for _, scenario := range fm.scenarios {
if scenario.ExpectValid {
matches = append(matches, scenario.Fixture)
}
}
return matches
}
// GetInvalidFixtures returns all fixtures that should be invalid.
func (fm *FixtureManager) GetInvalidFixtures() []string {
var matches []string
for _, scenario := range fm.scenarios {
if !scenario.ExpectValid {
matches = append(matches, scenario.Fixture)
}
}
return matches
}
// resolveFixturePath determines the full path to a fixture file.
func (fm *FixtureManager) resolveFixturePath(name string) string {
// If it's a direct path, use it
if strings.Contains(name, "/") {
return fm.ensureYamlExtension(filepath.Join(fm.basePath, name))
}
// Try to find the fixture in search directories
if foundPath := fm.searchInDirectories(name); foundPath != "" {
return foundPath
}
// Default to root level if not found
return fm.ensureYamlExtension(filepath.Join(fm.basePath, name))
}
// ensureYamlExtension adds YAML extension if not present.
func (fm *FixtureManager) ensureYamlExtension(path string) string {
hasYMLExt := strings.HasSuffix(path, appconstants.ActionFileExtYML)
hasYAMLExt := strings.HasSuffix(path, appconstants.ActionFileExtYAML)
if !hasYMLExt && !hasYAMLExt {
path += appconstants.ActionFileExtYML
}
return path
}
// searchInDirectories searches for fixture in predefined directories.
func (fm *FixtureManager) searchInDirectories(name string) string {
searchDirs := []string{
"actions/javascript",
"actions/composite",
"actions/docker",
"actions/invalid",
"", // root level
}
for _, dir := range searchDirs {
path := fm.buildSearchPath(dir, name)
if _, err := os.Stat(path); err == nil {
return path
}
}
return ""
}
// buildSearchPath constructs search path for a directory.
func (fm *FixtureManager) buildSearchPath(dir, name string) string {
var path string
if dir == "" {
path = filepath.Join(fm.basePath, name)
} else {
path = filepath.Join(fm.basePath, dir, name)
}
return fm.ensureYamlExtension(path)
}
// determineActionType infers action type from fixture name and content.
func (fm *FixtureManager) determineActionType(name, content string) ActionType {
// Check by name/path first
if actionType := fm.determineActionTypeByName(name); actionType != ActionTypeMinimal {
return actionType
}
// Fall back to content analysis
return fm.determineActionTypeByContent(content)
}
// determineActionTypeByName infers action type from fixture name or path.
func (fm *FixtureManager) determineActionTypeByName(name string) ActionType {
if strings.Contains(name, "javascript") || strings.Contains(name, "node") {
return ActionTypeJavaScript
}
if strings.Contains(name, "composite") {
return ActionTypeComposite
}
if strings.Contains(name, "docker") {
return ActionTypeDocker
}
if strings.Contains(name, "invalid") {
return ActionTypeInvalid
}
if strings.Contains(name, "minimal") {
return ActionTypeMinimal
}
return ActionTypeMinimal
}
// determineActionTypeByContent infers action type from YAML content.
func (fm *FixtureManager) determineActionTypeByContent(content string) ActionType {
if strings.Contains(content, `using: 'composite'`) || strings.Contains(content, `using: "composite"`) {
return ActionTypeComposite
}
if strings.Contains(content, `using: 'docker'`) || strings.Contains(content, `using: "docker"`) {
return ActionTypeDocker
}
if strings.Contains(content, `using: 'node`) {
return ActionTypeJavaScript
}
return ActionTypeMinimal
}
// determineConfigType determines the type of configuration fixture.
func (fm *FixtureManager) determineConfigType(name string) string {
if strings.Contains(name, "global") {
return appconstants.ScopeGlobal
}
if strings.Contains(name, "repo") {
return "repo-specific"
}
if strings.Contains(name, "user") {
return "user-specific"
}
return "generic"
}
// validateFixtureContent performs basic validation on fixture content.
func (fm *FixtureManager) validateFixtureContent(content string) bool {
// Basic YAML structure validation
var data map[string]any
if err := yaml.Unmarshal([]byte(content), &data); err != nil {
return false
}
// Check for required fields for valid actions
if _, hasName := data["name"]; !hasName {
return false
}
if _, hasDescription := data["description"]; !hasDescription {
return false
}
runs, hasRuns := data["runs"]
if !hasRuns {
return false
}
// Validate the runs section content more thoroughly
runsMap, ok := runs.(map[string]any)
if !ok {
return false // runs field exists but is not a map
}
using, hasUsing := runsMap["using"]
if !hasUsing {
return false // runs section exists but has no using field
}
usingStr, ok := using.(string)
if !ok {
return false // using field exists but is not a string
}
// Use the same validation logic as ValidateActionYML
if !isValidRuntime(usingStr) {
return false
}
return true
}
// isValidRuntime checks if the given runtime is valid for GitHub Actions.
// This is duplicated from internal/validator.go to avoid import cycle.
func isValidRuntime(runtime string) bool {
validRuntimes := []string{
"node12", // Legacy Node.js runtime (deprecated)
"node16", // Legacy Node.js runtime (deprecated)
"node20", // Current Node.js runtime
"docker", // Docker container runtime
"composite", // Composite action runtime
}
runtime = strings.TrimSpace(strings.ToLower(runtime))
for _, valid := range validRuntimes {
if runtime == valid {
return true
}
}
return false
}
// validateConfigContent validates configuration fixture content.
func (fm *FixtureManager) validateConfigContent(content string) bool {
var data map[string]any
return yaml.Unmarshal([]byte(content), &data) == nil
}
// scenarioMatchesTags checks if a scenario matches any of the provided tags.
func (fm *FixtureManager) scenarioMatchesTags(scenario *TestScenario, tags []string) bool {
if len(tags) == 0 {
return true
}
for _, tag := range tags {
for _, scenarioTag := range scenario.Tags {
if tag == scenarioTag {
return true
}
}
}
return false
}
// createDefaultScenarios creates a default scenarios file.
func (fm *FixtureManager) createDefaultScenarios(scenarioFile string) error {
// Ensure the directory exists
scenarioDir := filepath.Dir(scenarioFile)
// #nosec G301 -- test directory permissions
if err := os.MkdirAll(scenarioDir, appconstants.FilePermDir); err != nil {
return fmt.Errorf("failed to create scenarios directory: %w", err)
}
defaultScenarios := struct {
Scenarios []TestScenario `yaml:"scenarios"`
}{
Scenarios: []TestScenario{
{
ID: "simple-javascript",
Name: "Simple JavaScript Action",
Description: "Basic JavaScript action with minimal configuration",
ActionType: ActionTypeJavaScript,
Fixture: "actions/javascript/simple.yml",
ExpectValid: true,
ExpectError: false,
Tags: []string{"javascript", "basic", "valid"},
},
{
ID: "composite-basic",
Name: "Basic Composite Action",
Description: "Composite action with multiple steps",
ActionType: ActionTypeComposite,
Fixture: "actions/composite/basic.yml",
ExpectValid: true,
ExpectError: false,
Tags: []string{"composite", "basic", "valid"},
},
{
ID: "docker-basic",
Name: "Basic Docker Action",
Description: "Docker-based action with Dockerfile",
ActionType: ActionTypeDocker,
Fixture: "actions/docker/basic.yml",
ExpectValid: true,
ExpectError: false,
Tags: []string{"docker", "basic", "valid"},
},
{
ID: "invalid-missing-description",
Name: "Invalid Action - Missing Description",
Description: "Action missing required description field",
ActionType: ActionTypeInvalid,
Fixture: "actions/invalid/missing-description.yml",
ExpectValid: false,
ExpectError: true,
Tags: []string{"invalid", "validation", "error"},
},
},
}
data, err := yaml.Marshal(&defaultScenarios)
if err != nil {
return fmt.Errorf("failed to marshal default scenarios: %w", err)
}
if err := os.WriteFile(scenarioFile, data, 0600); err != nil {
return fmt.Errorf("failed to write scenarios file: %w", err)
}
// Load the scenarios we just created
return fm.LoadScenarios()
}
// Global fixture manager instance.
var defaultFixtureManager *FixtureManager
// GetFixtureManager returns the global fixture manager instance.
func GetFixtureManager() *FixtureManager {
if defaultFixtureManager == nil {
defaultFixtureManager = NewFixtureManager()
if err := defaultFixtureManager.LoadScenarios(); err != nil {
panic(fmt.Sprintf("failed to load test scenarios: %v", err))
}
}
return defaultFixtureManager
}
// Helper functions for backward compatibility and convenience
// LoadActionFixture loads an action fixture using the global fixture manager.
func LoadActionFixture(name string) (*ActionFixture, error) {
return GetFixtureManager().LoadActionFixture(name)
}
// LoadConfigFixture loads a config fixture using the global fixture manager.
func LoadConfigFixture(name string) (*ConfigFixture, error) {
return GetFixtureManager().LoadConfigFixture(name)
}
// GetFixturesByTag returns fixtures matching tags using the global fixture manager.
func GetFixturesByTag(tags ...string) []string {
return GetFixtureManager().GetFixturesByTag(tags...)
}
// GetFixturesByActionType returns fixtures by action type using the global fixture manager.
func GetFixturesByActionType(actionType ActionType) []string {
return GetFixtureManager().GetFixturesByActionType(actionType)
}
// GetValidFixtures returns all valid fixtures using the global fixture manager.
func GetValidFixtures() []string {
return GetFixtureManager().GetValidFixtures()
}
// GetInvalidFixtures returns all invalid fixtures using the global fixture manager.
func GetInvalidFixtures() []string {
return GetFixtureManager().GetInvalidFixtures()
}