feat: fixes, tweaks, new actions, linting (#186)

* feat: fixes, tweaks, new actions, linting
* fix: improve docker publish loops and dotnet parsing (#193)
* fix: harden action scripts and version checks (#191)
* refactor: major repository restructuring and security enhancements

Add comprehensive development infrastructure:
- Add Makefile with automated documentation generation, formatting, and linting tasks
- Add TODO.md tracking self-containment progress and repository improvements
- Add .nvmrc for consistent Node.js version management
- Create python-version-detect-v2 action for enhanced Python detection

Enhance all GitHub Actions with standardized patterns:
- Add consistent token handling across 27 actions using standardized input patterns
- Implement bash error handling (set -euo pipefail) in all shell steps
- Add comprehensive input validation for path traversal and command injection protection
- Standardize checkout token authentication to prevent rate limiting
- Remove relative action dependencies to ensure external usability

Rewrite security workflow for PR-focused analysis:
- Transform security-suite.yml to PR-only security analysis workflow
- Remove scheduled runs, repository issue management, and Slack notifications
- Implement smart comment generation showing only sections with content
- Add GitHub Actions permission diff analysis and new action detection
- Integrate OWASP, Semgrep, and TruffleHog for comprehensive PR security scanning

Improve version detection and dependency management:
- Simplify version detection actions to use inline logic instead of shared utilities
- Fix Makefile version detection fallback to properly return 'main' when version not found
- Update all external action references to use SHA-pinned versions
- Remove deprecated run.sh in favor of Makefile automation

Update documentation and project standards:
- Enhance CLAUDE.md with self-containment requirements and linting standards
- Update README.md with improved action descriptions and usage examples
- Standardize code formatting with updated .editorconfig and .prettierrc.yml
- Improve GitHub templates for issues and security reporting

This refactoring ensures all 40 actions are fully self-contained and can be used independently when
referenced as ivuorinen/actions/action-name@main, addressing the critical requirement for external
usability while maintaining comprehensive security analysis and development automation.

* feat: add automated action catalog generation system

- Create generate_listing.cjs script for comprehensive action catalog
- Add package.json with development tooling and npm scripts
- Implement automated README.md catalog section with --update flag
- Generate markdown reference-style links for all 40 actions
- Add categorized tables with features, language support matrices
- Replace static reference links with auto-generated dynamic links
- Enable complete automation of action documentation maintenance

* feat: enhance actions with improved documentation and functionality

- Add comprehensive README files for 12 actions with usage examples
- Implement new utility actions (go-version-detect, dotnet-version-detect)
- Enhance node-setup with extensive configuration options
- Improve error handling and validation across all actions
- Update package.json scripts for better development workflow
- Expand TODO.md with detailed roadmap and improvement plans
- Standardize action structure with consistent inputs/outputs

* feat: add comprehensive output handling across all actions

- Add standardized outputs to 15 actions that previously had none
- Implement consistent snake_case naming convention for all outputs
- Add build status and test results outputs to build actions
- Add files changed and status outputs to lint/fix actions
- Add test execution metrics to php-tests action
- Add stale/closed counts to stale action
- Add release URLs and IDs to github-release action
- Update documentation with output specifications
- Mark comprehensive output handling task as complete in TODO.md

* feat: implement shared cache strategy across all actions

- Add caching to 10 actions that previously had none (Node.js, .NET, Python, Go)
- Standardize 4 existing actions to use common-cache instead of direct actions/cache
- Implement consistent cache-hit optimization to skip installations when cache available
- Add language-specific cache configurations with appropriate key files
- Create unified caching approach using ivuorinen/actions/common-cache@main
- Fix YAML syntax error in php-composer action paths parameter
- Update TODO.md to mark shared cache strategy as complete

* feat: implement comprehensive retry logic for network operations

- Create new common-retry action for standardized retry patterns with configurable strategies
- Add retry logic to 9 actions missing network retry capabilities
- Implement exponential backoff, custom timeouts, and flexible error handling
- Add max-retries input parameter to all network-dependent actions (Node.js, .NET, Python, Go)
- Standardize existing retry implementations to use common-retry utility
- Update action catalog to include new common-retry action (41 total actions)
- Update documentation with retry configuration examples and parameters
- Mark retry logic implementation as complete in TODO.md roadmap

* feat: enhance Node.js support with Corepack and Bun

- Add Corepack support for automatic package manager version management
- Add Bun package manager support across all Node.js actions
- Improve Yarn Berry/PnP support with .yarnrc.yml detection
- Add Node.js feature detection (ESM, TypeScript, frameworks)
- Update package manager detection priority and lockfile support
- Enhance caching with package-manager-specific keys
- Update eslint, prettier, and biome actions for multi-package-manager support

* fix: resolve critical runtime issues across multiple actions

- Fix token validation by removing ineffective literal string comparisons
- Add missing @microsoft/eslint-formatter-sarif dependency for SARIF output
- Fix Bash variable syntax errors in username and changelog length checks
- Update Dockerfile version regex to handle tags with suffixes (e.g., -alpine)
- Simplify version selection logic with single grep command
- Fix command execution in retry action with proper bash -c wrapper
- Correct step output references using .outcome instead of .outputs.outcome
- Add missing step IDs for version detection actions
- Include go.mod in cache key files for accurate invalidation
- Require minor version in all version regex patterns
- Improve Bun installation security by verifying script before execution
- Replace bc with sort -V for portable PHP version comparison
- Remove non-existent pre-commit output references

These fixes ensure proper runtime behavior, improved security, and better
cross-platform compatibility across all affected actions.

* fix: resolve critical runtime and security issues across actions

- Fix biome-fix files_changed calculation using git diff instead of git status delta
- Fix compress-images output description and add absolute path validation
- Remove csharp-publish token default and fix token fallback in push commands
- Add @microsoft/eslint-formatter-sarif to all package managers in eslint-check
- Fix eslint-check command syntax by using variable assignment
- Improve node-setup Bun installation security and remove invalid frozen-lockfile flag
- Fix pre-commit token validation by removing ineffective literal comparison
- Fix prettier-fix token comparison and expand regex for all GitHub token types
- Add version-file-parser regex validation safety and fix csproj wildcard handling

These fixes address security vulnerabilities, runtime errors, and functional issues
to ensure reliable operation across all affected GitHub Actions.

* feat: enhance Docker actions with advanced multi-architecture support

Major enhancement to Docker build and publish actions with comprehensive
multi-architecture capabilities and enterprise-grade features.

Added features:
- Advanced buildx configuration (version control, cache modes, build contexts)
- Auto-detect platforms for dynamic architecture discovery
- Performance optimizations with enhanced caching strategies
- Security scanning with Trivy and image signing with Cosign
- SBOM generation in multiple formats with validation
- Verbose logging and dry-run modes for debugging
- Platform-specific build args and fallback mechanisms

Enhanced all Docker actions:
- docker-build: Core buildx features and multi-arch support
- docker-publish-gh: GitHub Packages with security features
- docker-publish-hub: Docker Hub with scanning and signing
- docker-publish: Orchestrator with unified configuration

Updated documentation across all modified actions.

* fix: resolve documentation generation placeholder issue

Fixed Makefile and package.json to properly replace placeholder tokens in generated documentation, ensuring all README files show correct repository paths instead of ***PROJECT***@***VERSION***.

* chore: simplify github token validation
* chore(lint): optional yamlfmt, config and fixes
* feat: use relative `uses` names

* feat: comprehensive testing infrastructure and Python validation system

- Migrate from tests/ to _tests/ directory structure with ShellSpec framework
- Add comprehensive validation system with Python-based input validation
- Implement dual testing approach (ShellSpec + pytest) for complete coverage
- Add modern Python tooling (uv, ruff, pytest-cov) and dependencies
- Create centralized validation rules with automatic generation system
- Update project configuration and build system for new architecture
- Enhance documentation to reflect current testing capabilities

This establishes a robust foundation for action validation and testing
with extensive coverage across all GitHub Actions in the repository.

* chore: remove Dockerfile for now
* chore: code review fixes

* feat: comprehensive GitHub Actions restructuring and tooling improvements

This commit represents a major restructuring of the GitHub Actions monorepo
with improved tooling, testing infrastructure, and comprehensive PR #186
review implementation.

## Major Changes

### 🔧 Development Tooling & Configuration
- **Shellcheck integration**: Exclude shellspec test files from linting
  - Updated .pre-commit-config.yaml to exclude _tests/*.sh from shellcheck/shfmt
  - Modified Makefile shellcheck pattern to skip shellspec files
  - Updated CLAUDE.md documentation with proper exclusion syntax
- **Testing infrastructure**: Enhanced Python validation framework
  - Fixed nested if statements and boolean parameter issues in validation.py
  - Improved code quality with explicit keyword arguments
  - All pre-commit hooks now passing

### 🏗️ Project Structure & Documentation
- **Added Serena AI integration** with comprehensive project memories:
  - Project overview, structure, and technical stack documentation
  - Code style conventions and completion requirements
  - Comprehensive PR #186 review analysis and implementation tracking
- **Enhanced configuration**: Updated .gitignore, .yamlfmt.yml, pyproject.toml
- **Improved testing**: Added integration workflows and enhanced test specs

### 🚀 GitHub Actions Improvements (30+ actions updated)
- **Centralized validation**: Updated 41 validation rule files
- **Enhanced actions**: Improvements across all action categories:
  - Setup actions (node-setup, version detectors)
  - Utility actions (version-file-parser, version-validator)
  - Linting actions (biome, eslint, terraform-lint-fix major refactor)
  - Build/publish actions (docker-build, npm-publish, csharp-*)
  - Repository management actions

### 📝 Documentation Updates
- **README consistency**: Updated version references across action READMEs
- **Enhanced documentation**: Improved action descriptions and usage examples
- **CLAUDE.md**: Updated with current tooling and best practices

## Technical Improvements
- **Security enhancements**: Input validation and sanitization improvements
- **Performance optimizations**: Streamlined action logic and dependencies
- **Cross-platform compatibility**: Better Windows/macOS/Linux support
- **Error handling**: Improved error reporting and user feedback

## Files Changed
- 100 files changed
- 13 new Serena memory files documenting project state
- 41 validation rules updated for consistency
- 30+ GitHub Actions and READMEs improved
- Core tooling configuration enhanced

* feat: comprehensive GitHub Actions improvements and PR review fixes

Major Infrastructure Improvements:
- Add comprehensive testing framework with 17+ ShellSpec validation tests
- Implement Docker-based testing tools with automated test runner
- Add CodeRabbit configuration for automated code reviews
- Restructure documentation and memory management system
- Update validation rules for 25+ actions with enhanced input validation
- Modernize CI/CD workflows and testing infrastructure

Critical PR Review Fixes (All Issues Resolved):
- Fix double caching in node-setup (eliminate redundant cache operations)
- Optimize shell pipeline in version-file-parser (single awk vs complex pipeline)
- Fix GitHub expression interpolation in prettier-check cache keys
- Resolve terraform command order issue (validation after setup)
- Add missing flake8-sarif dependency for Python SARIF output
- Fix environment variable scope in pr-lint (export to GITHUB_ENV)

Performance & Reliability:
- Eliminate duplicate cache operations saving CI time
- Improve shell script efficiency with optimized parsing
- Fix command execution dependencies preventing runtime failures
- Ensure proper dependency installation for all linting tools
- Resolve workflow conditional logic issues

Security & Quality:
- All input validation rules updated with latest security patterns
- Cross-platform compatibility improvements maintained
- Comprehensive error handling and retry logic preserved
- Modern development tooling and best practices adopted

This commit addresses 100% of actionable feedback from PR review analysis,
implements comprehensive testing infrastructure, and maintains high code
quality standards across all 41 GitHub Actions.

* feat: enhance expression handling and version parsing

- Fix node-setup force-version expression logic for proper empty string handling
- Improve version-file-parser with secure regex validation and enhanced Python detection
- Add CodeRabbit configuration for CalVer versioning and README review guidance

* feat(validate-inputs): implement modular validation system

- Add modular validator architecture with specialized validators
- Implement base validator classes for different input types
- Add validators: boolean, docker, file, network, numeric, security, token, version
- Add convention mapper for automatic input validation
- Add comprehensive documentation for the validation system
- Implement PCRE regex support and injection protection

* feat(validate-inputs): add validation rules for all actions

- Add YAML validation rules for 42 GitHub Actions
- Auto-generated rules with convention mappings
- Include metadata for validation coverage and quality indicators
- Mark rules as auto-generated to prevent manual edits

* test(validate-inputs): add comprehensive test suite for validators

- Add unit tests for all validator modules
- Add integration tests for the validation system
- Add fixtures for version test data
- Test coverage for boolean, docker, file, network, numeric, security, token, and version validators
- Add tests for convention mapper and registry

* feat(tools): add validation scripts and utilities

- Add update-validators.py script for auto-generating rules
- Add benchmark-validator.py for performance testing
- Add debug-validator.py for troubleshooting
- Add generate-tests.py for test generation
- Add check-rules-not-manually-edited.sh for CI validation
- Add fix-local-action-refs.py tool for fixing action references

* feat(actions): add CustomValidator.py files for specialized validation

- Add custom validators for actions requiring special validation logic
- Implement validators for docker, go, node, npm, php, python, terraform actions
- Add specialized validation for compress-images, common-cache, common-file-check
- Implement version detection validators with language-specific logic
- Add validation for build arguments, architectures, and version formats

* test: update ShellSpec test framework for Python validation

- Update all validation.spec.sh files to use Python validator
- Add shared validation_core.py for common test utilities
- Remove obsolete bash validation helpers
- Update test output expectations for Python validator format
- Add codeql-analysis test suite
- Refactor framework utilities for Python integration
- Remove deprecated test files

* feat(actions): update action.yml files to use validate-inputs

- Replace inline bash validation with validate-inputs action
- Standardize validation across all 42 actions
- Add new codeql-analysis action
- Update action metadata and branding
- Add validation step as first step in composite actions
- Maintain backward compatibility with existing inputs/outputs

* ci: update GitHub workflows for enhanced security and testing

- Add new codeql-new.yml workflow
- Update security scanning workflows
- Enhance dependency review configuration
- Update test-actions workflow for new validation system
- Improve workflow permissions and security settings
- Update action versions to latest SHA-pinned releases

* build: update build configuration and dependencies

- Update Makefile with new validation targets
- Add Python dependencies in pyproject.toml
- Update npm dependencies and scripts
- Enhance Docker testing tools configuration
- Add targets for validator updates and local ref fixes
- Configure uv for Python package management

* chore: update linting and documentation configuration

- Update EditorConfig settings for consistent formatting
- Enhance pre-commit hooks configuration
- Update prettier and yamllint ignore patterns
- Update gitleaks security scanning rules
- Update CodeRabbit review configuration
- Update CLAUDE.md with latest project standards and rules

* docs: update Serena memory files and project metadata

- Remove obsolete PR-186 memory files
- Update project overview with current architecture
- Update project structure documentation
- Add quality standards and communication guidelines
- Add modular validator architecture documentation
- Add shellspec testing framework documentation
- Update project.yml with latest configuration

* feat: moved rules.yml to same folder as action, fixes

* fix(validators): correct token patterns and fix validator bugs

- Fix GitHub classic PAT pattern: ghp_ + 36 chars = 40 total
- Fix GitHub fine-grained PAT pattern: github_pat_ + 71 chars = 82 total
- Initialize result variable in convention_mapper to prevent UnboundLocalError
- Fix empty URL validation in network validator to return error
- Add GitHub expression check to docker architectures validator
- Update docker-build CustomValidator parallel-builds max to 16

* test(validators): fix test fixtures and expectations

- Fix token lengths in test data: github_pat 71 chars, ghp/gho 36 chars
- Update integration tests with correct token lengths
- Fix file validator test to expect absolute paths rejected for security
- Rename TestGenerator import to avoid pytest collection warning
- Update custom validator tests with correct input names
- Change docker-build tests: platforms->architectures, tags->tag
- Update docker-publish tests to match new registry enum validation

* test(shellspec): fix token lengths in test helpers and specs

- Fix default token lengths in spec_helper.sh to use correct 40-char format
- Update csharp-publish default tokens in 4 locations
- Update codeql-analysis default tokens in 2 locations
- Fix codeql-analysis test tokens to correct lengths (40 and 82 chars)
- Fix npm-publish fine-grained token test to use 82-char format

* feat(actions): add permissions documentation and environment variable usage

- Add permissions comments to all action.yml files documenting required GitHub permissions
- Convert direct input usage to environment variables in shell steps for security
- Add validation steps with proper error handling
- Update input descriptions and add security notes where applicable
- Ensure all actions follow consistent patterns for input validation

* chore(workflows): update GitHub Actions workflow versions

- Update workflow action versions to latest
- Improve workflow consistency and maintainability

* docs(security): add comprehensive security policy

- Document security features and best practices
- Add vulnerability reporting process
- Include audit history and security testing information

* docs(memory): add GitHub workflow reference documentation

- Add GitHub Actions workflow commands reference
- Add GitHub workflow expressions guide
- Add secure workflow usage patterns and best practices

* chore: token optimization, code style conventions
* chore: cr fixes
* fix: trivy reported Dockerfile problems
* fix(security): more security fixes
* chore: dockerfile and make targets for publishing
* fix(ci): add creds to test-actions workflow
* fix: security fix and checkout step to codeql-new
* chore: test fixes
* fix(security): codeql detected issues
* chore: code review fixes, ReDos protection
* style: apply MegaLinter fixes
* fix(ci): missing packages read permission
* fix(ci): add missing working directory setting
* chore: linting, add validation-regex to use regex_pattern
* chore: code review fixes
* chore(deps): update actions
* fix(security): codeql fixes
* chore(cr): apply cr comments
* chore: improve POSIX compatibility
* chore(cr): apply cr comments
* fix: codeql warning in Dockerfile, build failures
* chore(cr): apply cr comments
* fix: docker-testing-tools/Dockerfile
* chore(cr): apply cr comments
* fix(docker): update testing-tools image for GitHub Actions compatibility
* chore(cr): apply cr comments
* feat: add more tests, fix issues
* chore: fix codeql issues, update actions
* chore(cr): apply cr comments
* fix: integration tests
* chore: deduplication and fixes
* style: apply MegaLinter fixes
* chore(cr): apply cr comments
* feat: dry-run mode for generate-tests
* fix(ci): kcov installation
* chore(cr): apply cr comments
* chore(cr): apply cr comments
* chore(cr): apply cr comments
* chore(cr): apply cr comments, simplify action testing, use uv
* fix: run-tests.sh action counting
* chore(cr): apply cr comments
* chore(cr): apply cr comments
This commit is contained in:
2025-10-14 13:37:58 +03:00
committed by GitHub
parent d3cc8d4790
commit 78fdad69e5
353 changed files with 55370 additions and 1714 deletions

675
_tests/README.md Normal file
View File

@@ -0,0 +1,675 @@
# GitHub Actions Testing Framework
A comprehensive testing framework for validating GitHub Actions in this monorepo. This guide covers everything from basic usage to advanced testing patterns.
## 🚀 Quick Start
```bash
# Run all tests
make test
# Run only unit tests
make test-unit
# Run tests for specific action
make test-action ACTION=node-setup
# Run with coverage reporting
make test-coverage
```
### Prerequisites
```bash
# Install ShellSpec (testing framework)
curl -fsSL https://github.com/shellspec/shellspec/releases/latest/download/shellspec-dist.tar.gz | tar -xz
sudo make -C shellspec-* install
# Install nektos/act (optional, for integration tests)
brew install act # macOS
# or: curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
```
## 📁 Framework Overview
### Architecture
The testing framework uses a **multi-level testing strategy**:
1. **Unit Tests** - Fast validation of action logic, inputs, and outputs
2. **Integration Tests** - Test actions in realistic workflow environments
3. **External Usage Tests** - Validate actions work as `ivuorinen/actions/action-name@main`
### Technology Stack
- **Primary Framework**: [ShellSpec](https://shellspec.info/) - BDD testing for shell scripts
- **Local Execution**: [nektos/act](https://github.com/nektos/act) - Run GitHub Actions locally
- **Coverage**: kcov integration for shell script coverage
- **Mocking**: Custom GitHub API and service mocks
- **CI Integration**: GitHub Actions workflows
### Directory Structure
```text
_tests/
├── README.md # This documentation
├── run-tests.sh # Main test runner script
├── framework/ # Core testing utilities
│ ├── setup.sh # Test environment setup
│ ├── utils.sh # Common testing functions
│ ├── validation_helpers.sh # Validation helper functions
│ ├── validation.py # Python validation utilities
│ └── mocks/ # Mock services (GitHub API, etc.)
├── unit/ # Unit tests by action
│ ├── version-file-parser/ # Example unit tests
│ ├── node-setup/ # Example unit tests
│ └── ... # One directory per action
├── integration/ # Integration tests
│ ├── workflows/ # Test workflows for nektos/act
│ └── external-usage/ # External reference tests
├── coverage/ # Coverage reports
└── reports/ # Test execution reports
```
## ✍️ Writing Tests
### Basic Unit Test Structure
```bash
#!/usr/bin/env shellspec
# _tests/unit/my-action/validation.spec.sh
Include _tests/framework/utils.sh
Describe "my-action validation"
ACTION_DIR="my-action"
ACTION_FILE="$ACTION_DIR/action.yml"
BeforeAll "init_testing_framework"
Context "input validation"
It "validates all inputs comprehensively"
# Use validation helpers for comprehensive testing
test_boolean_input "verbose"
test_boolean_input "dry-run"
# Numeric range validations (use test_input_validation helper)
test_input_validation "$ACTION_DIR" "max-retries" "1" "success"
test_input_validation "$ACTION_DIR" "max-retries" "10" "success"
test_input_validation "$ACTION_DIR" "timeout" "3600" "success"
# Enum validations (use test_input_validation helper)
test_input_validation "$ACTION_DIR" "strategy" "fast" "success"
test_input_validation "$ACTION_DIR" "format" "json" "success"
# Version validations (use test_input_validation helper)
test_input_validation "$ACTION_DIR" "tool-version" "1.0.0" "success"
# Security and path validations (use test_input_validation helper)
test_input_validation "$ACTION_DIR" "command" "echo test" "success"
test_input_validation "$ACTION_DIR" "working-directory" "." "success"
End
End
Context "action structure"
It "has valid structure and metadata"
test_standard_action_structure "$ACTION_FILE" "Expected Action Name"
End
End
End
```
### Integration Test Example
```yaml
# _tests/integration/workflows/my-action-test.yml
name: Test my-action Integration
on: workflow_dispatch
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Test action locally
id: test-local
uses: ./my-action
with:
required-input: 'test-value'
- name: Validate outputs
run: |
echo "Output: ${{ steps.test-local.outputs.result }}"
[[ -n "${{ steps.test-local.outputs.result }}" ]] || exit 1
- name: Test external reference
uses: ivuorinen/actions/my-action@main
with:
required-input: 'test-value'
```
## 🛠️ Testing Helpers
### Available Validation Helpers
The framework provides comprehensive validation helpers that handle common testing patterns:
#### Boolean Input Testing
```bash
test_boolean_input "verbose" # Tests: true, false, rejects invalid
test_boolean_input "enable-cache"
test_boolean_input "dry-run"
```
#### Numeric Range Testing
```bash
# Note: test_numeric_range_input helper is not yet implemented.
# Use test_input_validation with appropriate test values instead:
test_input_validation "$ACTION_DIR" "max-retries" "1" "success" # min value
test_input_validation "$ACTION_DIR" "max-retries" "10" "success" # max value
test_input_validation "$ACTION_DIR" "max-retries" "0" "failure" # below min
test_input_validation "$ACTION_DIR" "timeout" "3600" "success"
test_input_validation "$ACTION_DIR" "parallel-jobs" "8" "success"
```
#### Version Testing
```bash
# Note: test_version_input helper is not yet implemented.
# Use test_input_validation with appropriate test values instead:
test_input_validation "$ACTION_DIR" "version" "1.0.0" "success" # semver
test_input_validation "$ACTION_DIR" "version" "v1.0.0" "success" # v-prefix
test_input_validation "$ACTION_DIR" "version" "1.0.0-rc.1" "success" # pre-release
test_input_validation "$ACTION_DIR" "tool-version" "2.3.4" "success"
```
#### Enum Testing
```bash
# Note: test_enum_input helper is not yet implemented.
# Use test_input_validation with appropriate test values instead:
test_input_validation "$ACTION_DIR" "strategy" "linear" "success"
test_input_validation "$ACTION_DIR" "strategy" "exponential" "success"
test_input_validation "$ACTION_DIR" "strategy" "invalid" "failure"
test_input_validation "$ACTION_DIR" "format" "json" "success"
test_input_validation "$ACTION_DIR" "format" "yaml" "success"
```
#### Docker-Specific Testing
```bash
# Available framework helpers:
test_input_validation "$action_dir" "$input_name" "$test_value" "$expected_result"
test_action_outputs "$action_dir"
test_external_usage "$action_dir"
# Note: Docker-specific helpers (test_docker_image_input, test_docker_tag_input,
# test_docker_platforms_input) are referenced in examples but not yet implemented.
# Use test_input_validation with appropriate test values instead.
```
### Complete Action Validation Example
```bash
Describe "comprehensive-action validation"
ACTION_DIR="comprehensive-action"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "complete input validation"
It "validates all input types systematically"
# Boolean inputs
test_boolean_input "verbose"
test_boolean_input "enable-cache"
test_boolean_input "dry-run"
# Numeric ranges (use test_input_validation helper)
test_input_validation "$ACTION_DIR" "max-retries" "1" "success"
test_input_validation "$ACTION_DIR" "max-retries" "10" "success"
test_input_validation "$ACTION_DIR" "timeout" "3600" "success"
test_input_validation "$ACTION_DIR" "parallel-jobs" "8" "success"
# Enums (use test_input_validation helper)
test_input_validation "$ACTION_DIR" "strategy" "fast" "success"
test_input_validation "$ACTION_DIR" "format" "json" "success"
# Docker-specific (use test_input_validation helper)
test_input_validation "$ACTION_DIR" "image-name" "myapp:latest" "success"
test_input_validation "$ACTION_DIR" "tag" "1.0.0" "success"
test_input_validation "$ACTION_DIR" "platforms" "linux/amd64,linux/arm64" "success"
# Security validation (use test_input_validation helper)
test_input_validation "$ACTION_DIR" "command" "echo test" "success"
test_input_validation "$ACTION_DIR" "build-args" "ARG1=value" "success"
# Paths (use test_input_validation helper)
test_input_validation "$ACTION_DIR" "working-directory" "." "success"
test_input_validation "$ACTION_DIR" "output-directory" "./output" "success"
# Versions (use test_input_validation helper)
test_input_validation "$ACTION_DIR" "tool-version" "1.0.0" "success"
# Action structure
test_standard_action_structure "$ACTION_FILE" "Comprehensive Action"
End
End
End
```
## 🎯 Testing Patterns by Action Type
### Setup Actions (node-setup, php-version-detect, etc.)
Focus on version detection and environment setup:
```bash
Context "version detection"
It "detects version from config files"
create_mock_node_repo # or appropriate repo type
# Test version detection logic
export INPUT_LANGUAGE="node"
echo "detected-version=18.0.0" >> "$GITHUB_OUTPUT"
When call validate_action_output "detected-version" "18.0.0"
The status should be success
End
It "falls back to default when no version found"
# Use test_input_validation helper for version validation
test_input_validation "$ACTION_DIR" "default-version" "1.0.0" "success"
End
End
```
### Linting Actions (eslint-fix, prettier-fix, etc.)
Focus on file processing and fix capabilities:
```bash
Context "file processing"
BeforeEach "setup_test_env 'lint-test'"
AfterEach "cleanup_test_env 'lint-test'"
It "validates inputs and processes files"
test_boolean_input "fix-only"
# Use test_input_validation helper for path and security validations
test_input_validation "$ACTION_DIR" "working-directory" "." "success"
test_input_validation "$ACTION_DIR" "custom-command" "echo test" "success"
# Mock file processing
echo "files_changed=3" >> "$GITHUB_OUTPUT"
echo "status=changes_made" >> "$GITHUB_OUTPUT"
When call validate_action_output "status" "changes_made"
The status should be success
End
End
```
### Build Actions (docker-build, go-build, etc.)
Focus on build processes and artifact generation:
```bash
Context "build process"
BeforeEach "setup_test_env 'build-test'"
AfterEach "cleanup_test_env 'build-test'"
It "validates build inputs"
# Use test_input_validation helper for Docker inputs
test_input_validation "$ACTION_DIR" "image-name" "myapp:latest" "success"
test_input_validation "$ACTION_DIR" "tag" "1.0.0" "success"
test_input_validation "$ACTION_DIR" "platforms" "linux/amd64,linux/arm64" "success"
test_input_validation "$ACTION_DIR" "parallel-builds" "8" "success"
# Mock successful build
echo "build-status=success" >> "$GITHUB_OUTPUT"
echo "build-time=45" >> "$GITHUB_OUTPUT"
When call validate_action_output "build-status" "success"
The status should be success
End
End
```
### Publishing Actions (npm-publish, docker-publish, etc.)
Focus on registry interactions using mocks:
```bash
Context "publishing"
BeforeEach "setup_mock_environment"
AfterEach "cleanup_mock_environment"
It "validates publishing inputs"
# Use test_input_validation helper for version, security, and enum validations
test_input_validation "$ACTION_DIR" "package-version" "1.0.0" "success"
test_input_validation "$ACTION_DIR" "registry-token" "ghp_test123" "success"
test_input_validation "$ACTION_DIR" "registry" "npm" "success"
test_input_validation "$ACTION_DIR" "registry" "github" "success"
# Mock successful publish
echo "publish-status=success" >> "$GITHUB_OUTPUT"
echo "registry-url=https://registry.npmjs.org/" >> "$GITHUB_OUTPUT"
When call validate_action_output "publish-status" "success"
The status should be success
End
End
```
## 🔧 Running Tests
### Command Line Interface
```bash
# Basic usage
./_tests/run-tests.sh [OPTIONS] [ACTION_NAME...]
# Examples
./_tests/run-tests.sh # All tests, all actions
./_tests/run-tests.sh -t unit # Unit tests only
./_tests/run-tests.sh -a node-setup # Specific action
./_tests/run-tests.sh -t integration docker-build # Integration tests for docker-build
./_tests/run-tests.sh --format json --coverage # JSON output with coverage
```
### Options
| Option | Description |
|-----------------------|------------------------------------------------|
| `-t, --type TYPE` | Test type: `unit`, `integration`, `e2e`, `all` |
| `-a, --action ACTION` | Filter by action name pattern |
| `-j, --jobs JOBS` | Number of parallel jobs (default: 4) |
| `-c, --coverage` | Enable coverage reporting |
| `-f, --format FORMAT` | Output format: `console`, `json`, `junit` |
| `-v, --verbose` | Enable verbose output |
| `-h, --help` | Show help message |
### Make Targets
```bash
make test # Run all tests
make test-unit # Unit tests only
make test-integration # Integration tests only
make test-coverage # Tests with coverage
make test-action ACTION=name # Test specific action
```
## 🤝 Contributing Tests
### Adding Tests for New Actions
1. **Create Unit Test Directory**
```bash
mkdir -p _tests/unit/new-action
```
2. **Write Comprehensive Unit Tests**
```bash
# Copy template and customize
cp _tests/unit/version-file-parser/validation.spec.sh \
_tests/unit/new-action/validation.spec.sh
```
3. **Use Validation Helpers**
```bash
# Focus on using helpers for comprehensive coverage
test_boolean_input "verbose"
# Use test_input_validation helper for numeric, security, and other validations
test_input_validation "$ACTION_DIR" "timeout" "3600" "success"
test_input_validation "$ACTION_DIR" "command" "echo test" "success"
test_standard_action_structure "$ACTION_FILE" "New Action"
```
4. **Create Integration Test**
```bash
cp _tests/integration/workflows/version-file-parser-test.yml \
_tests/integration/workflows/new-action-test.yml
```
5. **Test Your Tests**
```bash
make test-action ACTION=new-action
```
### Pull Request Checklist
- [ ] Tests use validation helpers for common patterns
- [ ] All test types pass locally (`make test`)
- [ ] Integration test workflow created
- [ ] Security testing included for user inputs
- [ ] Tests are independent and isolated
- [ ] Proper cleanup in test teardown
- [ ] Documentation updated if needed
## 💡 Best Practices
### 1. Use Validation Helpers
✅ **Good**:
```bash
test_boolean_input "verbose"
# Use test_input_validation helper for other validations
test_input_validation "$ACTION_DIR" "timeout" "3600" "success"
test_input_validation "$ACTION_DIR" "format" "json" "success"
```
❌ **Avoid**:
```bash
# Don't write manual tests for boolean inputs when test_boolean_input exists
When call test_input_validation "$ACTION_DIR" "verbose" "true" "success"
When call test_input_validation "$ACTION_DIR" "verbose" "false" "success"
# Use test_boolean_input "verbose" instead
```
### 2. Group Related Validations
✅ **Good**:
```bash
Context "complete input validation"
It "validates all input types"
test_boolean_input "verbose"
# Use test_input_validation helper for other validations
test_input_validation "$ACTION_DIR" "timeout" "3600" "success"
test_input_validation "$ACTION_DIR" "format" "json" "success"
test_input_validation "$ACTION_DIR" "command" "echo test" "success"
End
End
```
### 3. Include Security Testing
✅ **Always include**:
```bash
# Use test_input_validation helper for security and path validations
test_input_validation "$ACTION_DIR" "command" "echo test" "success"
test_input_validation "$ACTION_DIR" "user-script" "#!/bin/bash" "success"
test_input_validation "$ACTION_DIR" "working-directory" "." "success"
```
### 4. Write Descriptive Test Names
✅ **Good**:
```bash
It "accepts valid semantic version format"
It "rejects version with invalid characters"
It "falls back to default when no version file exists"
```
❌ **Avoid**:
```bash
It "validates input"
It "works correctly"
```
### 5. Keep Tests Independent
- Each test should work in isolation
- Don't rely on test execution order
- Clean up after each test
- Use proper setup/teardown
## 🔍 Framework Features
### Test Environment Setup
```bash
# Setup test environment
setup_test_env "test-name"
# Create mock repositories
create_mock_repo "node" # Node.js project
create_mock_repo "php" # PHP project
create_mock_repo "python" # Python project
create_mock_repo "go" # Go project
create_mock_repo "dotnet" # .NET project
# Cleanup
cleanup_test_env "test-name"
```
### Mock Services
Built-in mocks for external services:
- **GitHub API** - Repository, releases, packages, workflows
- **NPM Registry** - Package publishing and retrieval
- **Docker Registry** - Image push/pull operations
- **Container Registries** - GitHub Container Registry, Docker Hub
### Available Environment Variables
```bash
# Test environment paths
$TEST_WORKSPACE # Current test workspace
$GITHUB_OUTPUT # Mock GitHub outputs file
$GITHUB_ENV # Mock GitHub environment file
$GITHUB_STEP_SUMMARY # Mock step summary file
# Test framework paths
$TEST_ROOT # _tests/ directory
$FRAMEWORK_DIR # _tests/framework/ directory
$FIXTURES_DIR # _tests/framework/fixtures/
$MOCKS_DIR # _tests/framework/mocks/
```
## 🚨 Troubleshooting
### Common Issues
#### "ShellSpec command not found"
```bash
# Install ShellSpec globally
curl -fsSL https://github.com/shellspec/shellspec/releases/latest/download/shellspec-dist.tar.gz | tar -xz
sudo make -C shellspec-* install
```
#### "act command not found"
```bash
# Install nektos/act (macOS)
brew install act
# Install nektos/act (Linux)
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
```
#### Tests timeout
```bash
# Increase timeout for slow operations
export TEST_TIMEOUT=300
```
#### Permission denied on test scripts
```bash
# Make test scripts executable
find _tests/ -name "*.sh" -exec chmod +x {} \;
```
### Debugging Tests
1. **Enable Verbose Mode**
```bash
./_tests/run-tests.sh -v
```
2. **Run Single Test**
```bash
shellspec _tests/unit/my-action/validation.spec.sh
```
3. **Check Test Output**
```bash
# Test results stored in _tests/reports/
cat _tests/reports/unit/my-action.txt
```
4. **Debug Mock Environment**
```bash
# Enable mock debugging
export MOCK_DEBUG=true
```
## 📚 Resources
- [ShellSpec Documentation](https://shellspec.info/)
- [nektos/act Documentation](https://nektosact.com/)
- [GitHub Actions Documentation](https://docs.github.com/en/actions)
- [Testing GitHub Actions Best Practices](https://docs.github.com/en/actions/creating-actions/creating-a-composite-action#testing-your-action)
---
## Framework Development
### Adding New Framework Features
1. **New Test Utilities**
```bash
# Add to _tests/framework/utils.sh
your_new_function() {
local param="$1"
# Implementation
}
# Export for availability
export -f your_new_function
```
2. **New Mock Services**
```bash
# Create _tests/framework/mocks/new-service.sh
# Follow existing patterns in github-api.sh
```
3. **New Validation Helpers**
```bash
# Add to _tests/framework/validation_helpers.sh
# Update this documentation
```
**Last Updated:** August 17, 2025

239
_tests/framework/setup.sh Executable file
View File

@@ -0,0 +1,239 @@
#!/usr/bin/env bash
# Test environment setup utilities
# Provides common setup functions for GitHub Actions testing
set -euo pipefail
# Global test configuration
export GITHUB_ACTIONS=true
export GITHUB_WORKSPACE="${GITHUB_WORKSPACE:-$(pwd)}"
export GITHUB_REPOSITORY="${GITHUB_REPOSITORY:-ivuorinen/actions}"
export GITHUB_SHA="${GITHUB_SHA:-fake-sha}"
export GITHUB_REF="${GITHUB_REF:-refs/heads/main}"
export GITHUB_TOKEN="${GITHUB_TOKEN:-ghp_fake_token_for_testing}"
# Test framework directories
TEST_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
FRAMEWORK_DIR="${TEST_ROOT}/framework"
FIXTURES_DIR="${FRAMEWORK_DIR}/fixtures"
MOCKS_DIR="${FRAMEWORK_DIR}/mocks"
# Export directories for use by other scripts
export FIXTURES_DIR MOCKS_DIR
# Only create TEMP_DIR if not already set
if [ -z "${TEMP_DIR:-}" ]; then
TEMP_DIR=$(mktemp -d) || exit 1
fi
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $*" >&2
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $*" >&2
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $*" >&2
}
log_error() {
echo -e "${RED}[ERROR]${NC} $*" >&2
}
# Setup test environment
setup_test_env() {
local test_name="${1:-unknown}"
log_info "Setting up test environment for: $test_name"
# Create temporary directory for test
export TEST_TEMP_DIR="${TEMP_DIR}/${test_name}"
mkdir -p "$TEST_TEMP_DIR"
# Create fake GitHub workspace
export TEST_WORKSPACE="${TEST_TEMP_DIR}/workspace"
mkdir -p "$TEST_WORKSPACE"
# Setup fake GitHub outputs
export GITHUB_OUTPUT="${TEST_TEMP_DIR}/github-output"
export GITHUB_ENV="${TEST_TEMP_DIR}/github-env"
export GITHUB_PATH="${TEST_TEMP_DIR}/github-path"
export GITHUB_STEP_SUMMARY="${TEST_TEMP_DIR}/github-step-summary"
# Initialize output files
touch "$GITHUB_OUTPUT" "$GITHUB_ENV" "$GITHUB_PATH" "$GITHUB_STEP_SUMMARY"
# Change to test workspace
cd "$TEST_WORKSPACE"
log_success "Test environment setup complete"
}
# Cleanup test environment
cleanup_test_env() {
local test_name="${1:-unknown}"
log_info "Cleaning up test environment for: $test_name"
if [[ -n ${TEST_TEMP_DIR:-} && -d $TEST_TEMP_DIR ]]; then
# Check if current directory is inside TEST_TEMP_DIR
local current_dir
current_dir="$(pwd)"
if [[ "$current_dir" == "$TEST_TEMP_DIR"* ]]; then
cd "$GITHUB_WORKSPACE" || cd /tmp || true
fi
rm -rf "$TEST_TEMP_DIR"
log_success "Test environment cleanup complete"
fi
}
# Cleanup framework temp directory
cleanup_framework_temp() {
if [[ -n ${TEMP_DIR:-} && -d $TEMP_DIR ]]; then
# Check if current directory is inside TEMP_DIR
local current_dir
current_dir="$(pwd)"
if [[ "$current_dir" == "$TEMP_DIR"* ]]; then
cd "$GITHUB_WORKSPACE" || cd /tmp || true
fi
rm -rf "$TEMP_DIR"
log_info "Framework temp directory cleaned up"
fi
}
# Create a mock GitHub repository structure
create_mock_repo() {
local repo_type="${1:-node}"
case "$repo_type" in
"node")
create_mock_node_repo
;;
"php" | "python" | "go" | "dotnet")
log_error "Unsupported repo type: $repo_type. Only 'node' is currently supported."
return 1
;;
*)
log_warning "Unknown repo type: $repo_type, defaulting to node"
create_mock_node_repo
;;
esac
}
# Create mock Node.js repository
create_mock_node_repo() {
cat >package.json <<EOF
{
"name": "test-project",
"version": "1.0.0",
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"test": "npm test",
"lint": "eslint .",
"build": "npm run build"
},
"devDependencies": {
"eslint": "^8.0.0"
}
}
EOF
echo "node_modules/" >.gitignore
mkdir -p src
echo 'console.log("Hello, World!");' >src/index.js
}
# Removed unused mock repository functions:
# create_mock_php_repo, create_mock_python_repo, create_mock_go_repo, create_mock_dotnet_repo
# Only create_mock_node_repo is used and kept below
# Validate action outputs
validate_action_output() {
local expected_key="$1"
local expected_value="$2"
local output_file="${3:-$GITHUB_OUTPUT}"
if grep -q "^${expected_key}=${expected_value}$" "$output_file"; then
log_success "Output validation passed: $expected_key=$expected_value"
return 0
else
log_error "Output validation failed: $expected_key=$expected_value not found"
log_error "Actual outputs:"
cat "$output_file" >&2
return 1
fi
}
# Removed unused function: run_action_step
# Check if required tools are available
check_required_tools() {
local tools=("git" "jq" "curl" "python3" "tar" "make")
local missing_tools=()
for tool in "${tools[@]}"; do
if ! command -v "$tool" >/dev/null 2>&1; then
missing_tools+=("$tool")
fi
done
if [[ ${#missing_tools[@]} -gt 0 ]]; then
log_error "Missing required tools: ${missing_tools[*]}"
return 1
fi
if [[ -z ${SHELLSPEC_VERSION:-} ]]; then
log_success "All required tools are available"
fi
return 0
}
# Initialize testing framework
init_testing_framework() {
# Use file-based lock to prevent multiple initialization across ShellSpec processes
local lock_file="${TEMP_DIR}/.framework_initialized"
if [[ -f "$lock_file" ]]; then
return 0
fi
# Silent initialization in ShellSpec environment to avoid output interference
if [[ -z ${SHELLSPEC_VERSION:-} ]]; then
log_info "Initializing GitHub Actions Testing Framework"
fi
# Check requirements
check_required_tools
# Temporary directory already created by mktemp above
# Note: Cleanup trap removed to avoid conflicts with ShellSpec
# Individual tests should call cleanup_test_env when needed
# Mark as initialized with file lock
touch "$lock_file"
export TESTING_FRAMEWORK_INITIALIZED=1
if [[ -z ${SHELLSPEC_VERSION:-} ]]; then
log_success "Testing framework initialized"
fi
}
# Export all functions for use in tests
export -f setup_test_env cleanup_test_env cleanup_framework_temp create_mock_repo
export -f create_mock_node_repo validate_action_output check_required_tools
export -f log_info log_success log_warning log_error
export -f init_testing_framework

352
_tests/framework/utils.sh Executable file
View File

@@ -0,0 +1,352 @@
#!/usr/bin/env bash
# Common testing utilities for GitHub Actions
# Provides helper functions for testing action behavior
set -euo pipefail
# Source setup utilities
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# shellcheck source=_tests/framework/setup.sh
source "${SCRIPT_DIR}/setup.sh"
# Action testing utilities
validate_action_yml() {
local action_file="$1"
local quiet_mode="${2:-false}"
if [[ ! -f $action_file ]]; then
[[ $quiet_mode == "false" ]] && log_error "Action file not found: $action_file"
return 1
fi
# Check if it's valid YAML
if ! yq eval '.' "$action_file" >/dev/null 2>&1; then
# Compute path relative to this script for CWD independence
local utils_dir
utils_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if ! uv run "$utils_dir/../shared/validation_core.py" --validate-yaml "$action_file" 2>/dev/null; then
[[ $quiet_mode == "false" ]] && log_error "Invalid YAML in action file: $action_file"
return 1
fi
fi
[[ $quiet_mode == "false" ]] && log_success "Action YAML is valid: $action_file"
return 0
}
# Extract action metadata using Python validation module
get_action_inputs() {
local action_file="$1"
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
uv run "$script_dir/../shared/validation_core.py" --inputs "$action_file"
}
get_action_outputs() {
local action_file="$1"
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
uv run "$script_dir/../shared/validation_core.py" --outputs "$action_file"
}
get_action_name() {
local action_file="$1"
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
uv run "$script_dir/../shared/validation_core.py" --name "$action_file"
}
# Test input validation using Python validation module
test_input_validation() {
local action_dir="$1"
local input_name="$2"
local test_value="$3"
local expected_result="${4:-success}" # success or failure
# Normalize action_dir to absolute path before setup_test_env changes working directory
action_dir="$(cd "$action_dir" && pwd)"
log_info "Testing input validation: $input_name = '$test_value'"
# Setup test environment
setup_test_env "input-validation-${input_name}"
# Use Python validation module via CLI
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
local result="success"
# Call validation_core CLI with proper argument passing (no injection risk)
if ! uv run "$script_dir/../shared/validation_core.py" --validate "$action_dir" "$input_name" "$test_value" 2>&1; then
result="failure"
fi
# Check result matches expectation
if [[ $result == "$expected_result" ]]; then
log_success "Input validation test passed: $input_name"
cleanup_test_env "input-validation-${input_name}"
return 0
else
log_error "Input validation test failed: $input_name (expected: $expected_result, got: $result)"
cleanup_test_env "input-validation-${input_name}"
return 1
fi
}
# Removed: create_validation_script, create_python_validation_script,
# convert_github_expressions_to_env_vars, needs_python_validation, python_validate_input
# These functions are no longer needed as we use Python validation directly
# Test action outputs
test_action_outputs() {
local action_dir="$1"
shift
# Normalize action_dir to absolute path before setup_test_env changes working directory
action_dir="$(cd "$action_dir" && pwd)"
log_info "Testing action outputs for: $(basename "$action_dir")"
# Setup test environment
setup_test_env "output-test-$(basename "$action_dir")"
create_mock_repo "node"
# Set up inputs
while [[ $# -gt 1 ]]; do
local key="$1"
local value="$2"
# Convert dashes to underscores and uppercase for environment variable names
local env_key="${key//-/_}"
local env_key_upper
env_key_upper=$(echo "$env_key" | tr '[:lower:]' '[:upper:]')
export "INPUT_${env_key_upper}"="$value"
shift 2
done
# Run the action (simplified simulation)
local action_file="${action_dir}/action.yml"
local action_name
action_name=$(get_action_name "$action_file")
log_info "Simulating action: $action_name"
# For now, we'll create mock outputs based on the action definition
local outputs
outputs=$(get_action_outputs "$action_file")
# Create mock outputs
while IFS= read -r output; do
if [[ -n $output ]]; then
echo "${output}=mock-value-$(date +%s)" >>"$GITHUB_OUTPUT"
fi
done <<<"$outputs"
# Validate outputs exist
local test_passed=true
while IFS= read -r output; do
if [[ -n $output ]]; then
if ! grep -q "^${output}=" "$GITHUB_OUTPUT"; then
log_error "Missing output: $output"
test_passed=false
else
log_success "Output found: $output"
fi
fi
done <<<"$outputs"
cleanup_test_env "output-test-$(basename "$action_dir")"
if [[ $test_passed == "true" ]]; then
log_success "Output test passed for: $(basename "$action_dir")"
return 0
else
log_error "Output test failed for: $(basename "$action_dir")"
return 1
fi
}
# Test external usage pattern
test_external_usage() {
local action_name="$1"
log_info "Testing external usage pattern for: $action_name"
# Create test workflow that uses external reference
local test_workflow_dir="${TEST_ROOT}/integration/workflows"
mkdir -p "$test_workflow_dir"
local workflow_file="${test_workflow_dir}/${action_name}-external-test.yml"
cat >"$workflow_file" <<EOF
name: External Usage Test - $action_name
on:
workflow_dispatch:
push:
paths:
- '$action_name/**'
jobs:
test-external-usage:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Test external usage
uses: ivuorinen/actions/${action_name}@main
with:
# Default inputs for testing
EOF
# Add common test inputs based on action type
case "$action_name" in
*-setup | *-version-detect)
echo " # Version detection action - no additional inputs needed" >>"$workflow_file"
;;
*-lint* | *-fix)
# shellcheck disable=SC2016
echo ' token: ${{ github.token }}' >>"$workflow_file"
;;
*-publish | *-build)
# shellcheck disable=SC2016
echo ' token: ${{ github.token }}' >>"$workflow_file"
;;
*)
echo " # Generic test inputs" >>"$workflow_file"
;;
esac
log_success "Created external usage test workflow: $workflow_file"
return 0
}
# Performance test utilities
measure_action_time() {
local action_dir="$1"
shift
# Normalize action_dir to absolute path for consistent behavior
action_dir="$(cd "$action_dir" && pwd)"
log_info "Measuring execution time for: $(basename "$action_dir")"
local start_time
start_time=$(date +%s%N)
# Run the action test
test_action_outputs "$action_dir" "$@"
local result=$?
local end_time
end_time=$(date +%s%N)
local duration_ns=$((end_time - start_time))
local duration_ms=$((duration_ns / 1000000))
log_info "Action execution time: ${duration_ms}ms"
# Store performance data
echo "$(basename "$action_dir"),${duration_ms}" >>"${TEST_ROOT}/reports/performance.csv"
return $result
}
# Batch test runner
run_action_tests() {
local action_dir="$1"
local test_type="${2:-all}" # all, unit, integration, outputs
# Normalize action_dir to absolute path for consistent behavior
action_dir="$(cd "$action_dir" && pwd)"
local action_name
action_name=$(basename "$action_dir")
log_info "Running $test_type tests for: $action_name"
local test_results=()
# Handle "all" type by running all test types
if [[ $test_type == "all" ]]; then
# Run unit tests
log_info "Running unit tests..."
if validate_action_yml "${action_dir}/action.yml"; then
test_results+=("unit:PASS")
else
test_results+=("unit:FAIL")
fi
# Run output tests
log_info "Running output tests..."
if test_action_outputs "$action_dir"; then
test_results+=("outputs:PASS")
else
test_results+=("outputs:FAIL")
fi
# Run integration tests
log_info "Running integration tests..."
if test_external_usage "$action_name"; then
test_results+=("integration:PASS")
else
test_results+=("integration:FAIL")
fi
else
# Handle individual test types
case "$test_type" in
"unit")
log_info "Running unit tests..."
if validate_action_yml "${action_dir}/action.yml"; then
test_results+=("unit:PASS")
else
test_results+=("unit:FAIL")
fi
;;
"outputs")
log_info "Running output tests..."
if test_action_outputs "$action_dir"; then
test_results+=("outputs:PASS")
else
test_results+=("outputs:FAIL")
fi
;;
"integration")
log_info "Running integration tests..."
if test_external_usage "$action_name"; then
test_results+=("integration:PASS")
else
test_results+=("integration:FAIL")
fi
;;
esac
fi
# Report results
log_info "Test results for $action_name:"
for result in "${test_results[@]}"; do
local test_name="${result%:*}"
local status="${result#*:}"
if [[ $status == "PASS" ]]; then
log_success " $test_name: $status"
else
log_error " $test_name: $status"
fi
done
# Check if all tests passed
if [[ ! " ${test_results[*]} " =~ " FAIL" ]]; then
log_success "All tests passed for: $action_name"
return 0
else
log_error "Some tests failed for: $action_name"
return 1
fi
}
# Export all functions
export -f validate_action_yml get_action_inputs get_action_outputs get_action_name
export -f test_input_validation test_action_outputs test_external_usage measure_action_time run_action_tests

885
_tests/framework/validation.py Executable file
View File

@@ -0,0 +1,885 @@
#!/usr/bin/env python3
"""
GitHub Actions Validation Module
This module provides advanced validation capabilities for GitHub Actions testing,
specifically handling PCRE regex patterns with lookahead/lookbehind assertions
that are not supported in bash's basic regex engine.
Features:
- PCRE-compatible regex validation using Python's re module
- GitHub token format validation with proper lookahead support
- Input sanitization and security validation
- Complex pattern detection and validation
"""
from __future__ import annotations
from pathlib import Path
import re
import sys
import yaml # pylint: disable=import-error
class ActionValidator:
"""Handles validation of GitHub Action inputs using Python regex engine."""
# Common regex patterns that require PCRE features
COMPLEX_PATTERNS = {
"lookahead": r"\(\?\=",
"lookbehind": r"\(\?\<=",
"negative_lookahead": r"\(\?\!",
"named_groups": r"\(\?P<\w+>",
"conditional": r"\(\?\(",
}
# Standardized token patterns (resolved GitHub documentation discrepancies)
# Fine-grained PATs are 50-255 characters with underscores (github_pat_[A-Za-z0-9_]{50,255})
TOKEN_PATTERNS = {
"classic": r"^gh[efpousr]_[a-zA-Z0-9]{36}$",
"fine_grained": r"^github_pat_[A-Za-z0-9_]{50,255}$", # 50-255 chars with underscores
"installation": r"^ghs_[a-zA-Z0-9]{36}$",
"npm_classic": r"^npm_[a-zA-Z0-9]{40,}$", # NPM classic tokens
}
def __init__(self):
"""Initialize the validator."""
def is_complex_pattern(self, pattern: str) -> bool:
"""
Check if a regex pattern requires PCRE features not supported in bash.
Args:
pattern: The regex pattern to check
Returns:
True if pattern requires PCRE features, False otherwise
"""
for regex in self.COMPLEX_PATTERNS.values():
if re.search(regex, pattern):
return True
return False
def validate_github_token(self, token: str, action_dir: str = "") -> tuple[bool, str]:
"""
Validate GitHub token format using proper PCRE patterns.
Args:
token: The token to validate
action_dir: The action directory (for context-specific validation)
Returns:
Tuple of (is_valid, error_message)
"""
# Actions that require tokens shouldn't accept empty values
action_name = Path(action_dir).name
if action_name in ["csharp-publish", "eslint-fix", "pr-lint", "pre-commit"]:
if not token or token.strip() == "":
return False, "Token cannot be empty"
# Other actions may accept empty tokens (they'll use defaults)
elif not token or token.strip() == "":
return True, ""
# Check for GitHub Actions expression (should be allowed)
if token == "${{ github.token }}" or (token.startswith("${{") and token.endswith("}}")):
return True, ""
# Check for environment variable reference (e.g., $GITHUB_TOKEN)
if re.match(r"^\$[A-Za-z_][A-Za-z0-9_]*$", token):
return True, ""
# Check against all known token patterns
for pattern in self.TOKEN_PATTERNS.values():
if re.match(pattern, token):
return True, ""
return (
False,
"Invalid token format. Expected: gh[efpousr]_* (36 chars), "
"github_pat_[A-Za-z0-9_]* (50-255 chars), ghs_* (36 chars), or npm_* (40+ chars)",
)
def validate_namespace_with_lookahead(self, namespace: str) -> tuple[bool, str]:
"""
Validate namespace using the original lookahead pattern from csharp-publish.
Args:
namespace: The namespace to validate
Returns:
Tuple of (is_valid, error_message)
"""
if not namespace or namespace.strip() == "":
return False, "Namespace cannot be empty"
# Original pattern: ^[a-zA-Z0-9]([a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$
# This ensures hyphens are only allowed when followed by alphanumeric characters
pattern = r"^[a-zA-Z0-9]([a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$"
if re.match(pattern, namespace):
return True, ""
return (
False,
"Invalid namespace format. Must be 1-39 characters, "
"alphanumeric and hyphens, no trailing hyphens",
)
def validate_input_pattern(self, input_value: str, pattern: str) -> tuple[bool, str]:
"""
Validate an input value against a regex pattern using Python's re module.
Args:
input_value: The value to validate
pattern: The regex pattern to match against
Returns:
Tuple of (is_valid, error_message)
"""
try:
if re.match(pattern, input_value):
return True, ""
return False, f"Value '{input_value}' does not match required pattern: {pattern}"
except re.error as e:
return False, f"Invalid regex pattern: {pattern} - {e!s}"
def validate_security_patterns(self, input_value: str) -> tuple[bool, str]:
"""
Check for common security injection patterns.
Args:
input_value: The value to validate
Returns:
Tuple of (is_valid, error_message)
"""
# Allow empty values for most inputs (they're often optional)
if not input_value or input_value.strip() == "":
return True, ""
# Common injection patterns
injection_patterns = [
r";\s*(rm|del|format|shutdown|reboot)",
r"&&\s*(rm|del|format|shutdown|reboot)",
r"\|\s*(rm|del|format|shutdown|reboot)",
r"`[^`]*`", # Command substitution
r"\$\([^)]*\)", # Command substitution
# Path traversal only dangerous when combined with commands
r"\.\./.*;\s*(rm|del|format|shutdown|reboot)",
r"\\\.\\\.\\.*;\s*(rm|del|format|shutdown|reboot)",
]
for pattern in injection_patterns:
if re.search(pattern, input_value, re.IGNORECASE):
return False, f"Potential security injection pattern detected: {pattern}"
return True, ""
def extract_validation_patterns(action_file: str) -> dict[str, list[str]]:
"""
Extract validation patterns from an action.yml file.
Args:
action_file: Path to the action.yml file
Returns:
Dictionary mapping input names to their validation patterns
"""
patterns = {}
try:
with Path(action_file).open(encoding="utf-8") as f:
content = f.read()
# Look for validation patterns in the shell scripts
validation_block_match = re.search(
r"- name:\s*Validate\s+Inputs.*?run:\s*\|(.+?)(?=- name:|$)",
content,
re.DOTALL | re.IGNORECASE,
)
if validation_block_match:
validation_script = validation_block_match.group(1)
# Extract regex patterns from the validation script
regex_matches = re.findall(
r'\[\[\s*["\']?\$\{\{\s*inputs\.(\w+(?:-\w+)*)\s*\}\}["\']?\s*=~\s*(.+?)\]\]',
validation_script,
re.DOTALL | re.IGNORECASE,
)
for input_name, pattern in regex_matches:
# Clean up the pattern
pattern = pattern.strip().strip("\"'")
if input_name not in patterns:
patterns[input_name] = []
patterns[input_name].append(pattern)
except Exception as e: # pylint: disable=broad-exception-caught
print(f"Error extracting patterns from {action_file}: {e}", file=sys.stderr)
return patterns
def get_input_property(action_file: str, input_name: str, property_check: str) -> str: # pylint: disable=too-many-return-statements
"""
Get a property of an input from an action.yml file.
This function replaces the functionality of check_input.py.
Args:
action_file: Path to the action.yml file
input_name: Name of the input to check
property_check: Property to check (required, optional, default, description, all_optional)
Returns:
- For 'required': 'required' or 'optional'
- For 'optional': 'optional' or 'required'
- For 'default': the default value or 'no-default'
- For 'description': the description or 'no-description'
- For 'all_optional': 'none' if no required inputs, else comma-separated list of
required inputs
"""
try:
with Path(action_file).open(encoding="utf-8") as f:
data = yaml.safe_load(f)
inputs = data.get("inputs", {})
input_data = inputs.get(input_name, {})
if property_check in ["required", "optional"]:
is_required = input_data.get("required") in [True, "true"]
if property_check == "required":
return "required" if is_required else "optional"
# optional
return "optional" if not is_required else "required"
if property_check == "default":
default_value = input_data.get("default", "")
return str(default_value) if default_value else "no-default"
if property_check == "description":
description = input_data.get("description", "")
return description if description else "no-description"
if property_check == "all_optional":
# Check if all inputs are optional (none are required)
required_inputs = [k for k, v in inputs.items() if v.get("required") in [True, "true"]]
return "none" if not required_inputs else ",".join(required_inputs)
return f"unknown-property-{property_check}"
except Exception as e: # pylint: disable=broad-exception-caught
return f"error: {e}"
def get_action_inputs(action_file: str) -> list[str]:
"""
Get all input names from an action.yml file.
This function replaces the bash version in utils.sh.
Args:
action_file: Path to the action.yml file
Returns:
List of input names
"""
try:
with Path(action_file).open(encoding="utf-8") as f:
data = yaml.safe_load(f)
inputs = data.get("inputs", {})
return list(inputs.keys())
except Exception:
return []
def get_action_outputs(action_file: str) -> list[str]:
"""
Get all output names from an action.yml file.
This function replaces the bash version in utils.sh.
Args:
action_file: Path to the action.yml file
Returns:
List of output names
"""
try:
with Path(action_file).open(encoding="utf-8") as f:
data = yaml.safe_load(f)
outputs = data.get("outputs", {})
return list(outputs.keys())
except Exception:
return []
def get_action_name(action_file: str) -> str:
"""
Get the action name from an action.yml file.
This function replaces the bash version in utils.sh.
Args:
action_file: Path to the action.yml file
Returns:
Action name or "Unknown" if not found
"""
try:
with Path(action_file).open(encoding="utf-8") as f:
data = yaml.safe_load(f)
return data.get("name", "Unknown")
except Exception:
return "Unknown"
def _show_usage():
"""Show usage information and exit."""
print("Usage:")
print(
" Validation mode: python3 validation.py <action_dir> <input_name> <input_value> "
"[expected_result]",
)
print(
" Property mode: python3 validation.py --property <action_file> <input_name> <property>",
)
print(" List inputs: python3 validation.py --inputs <action_file>")
print(" List outputs: python3 validation.py --outputs <action_file>")
print(" Get name: python3 validation.py --name <action_file>")
sys.exit(1)
def _parse_property_mode():
"""Parse property mode arguments."""
if len(sys.argv) != 5:
print(
"Property mode usage: python3 validation.py --property <action_file> "
"<input_name> <property>",
)
print("Properties: required, optional, default, description, all_optional")
sys.exit(1)
return {
"mode": "property",
"action_file": sys.argv[2],
"input_name": sys.argv[3],
"property": sys.argv[4],
}
def _parse_single_file_mode(mode_name):
"""Parse modes that take a single action file argument."""
if len(sys.argv) != 3:
print(f"{mode_name.title()} mode usage: python3 validation.py --{mode_name} <action_file>")
sys.exit(1)
return {
"mode": mode_name,
"action_file": sys.argv[2],
}
def _parse_validation_mode():
"""Parse validation mode arguments."""
if len(sys.argv) < 4:
print(
"Validation mode usage: python3 validation.py <action_dir> <input_name> "
"<input_value> [expected_result]",
)
print("Expected result: 'success' or 'failure' (default: auto-detect)")
sys.exit(1)
return {
"mode": "validation",
"action_dir": sys.argv[1],
"input_name": sys.argv[2],
"input_value": sys.argv[3],
"expected_result": sys.argv[4] if len(sys.argv) > 4 else None,
}
def _parse_command_line_args():
"""Parse and validate command line arguments."""
if len(sys.argv) < 2:
_show_usage()
mode_arg = sys.argv[1]
if mode_arg == "--property":
return _parse_property_mode()
if mode_arg in ["--inputs", "--outputs", "--name"]:
return _parse_single_file_mode(mode_arg[2:]) # Remove '--' prefix
return _parse_validation_mode()
def _resolve_action_file_path(action_dir: str) -> str:
"""Resolve the path to the action.yml file."""
action_dir_path = Path(action_dir)
if not action_dir_path.is_absolute():
# If relative, assume we're in _tests/framework and actions are at ../../
script_dir = Path(__file__).resolve().parent
project_root = script_dir.parent.parent
return str(project_root / action_dir / "action.yml")
return f"{action_dir}/action.yml"
def _validate_docker_build_input(input_name: str, input_value: str) -> tuple[bool, str]:
"""Handle special validation for docker-build inputs."""
if input_name == "build-args" and input_value == "":
return True, ""
# All other docker-build inputs pass through centralized validation
return True, ""
# Validation function registry
def _validate_boolean(input_value: str, input_name: str) -> tuple[bool, str]:
"""Validate boolean input."""
if input_value.lower() not in ["true", "false"]:
return False, f"Input '{input_name}' must be 'true' or 'false'"
return True, ""
def _validate_docker_architectures(input_value: str) -> tuple[bool, str]:
"""Validate docker architectures format."""
if input_value and not re.match(r"^[a-zA-Z0-9/_,.-]+$", input_value):
return False, f"Invalid docker architectures format: {input_value}"
return True, ""
def _validate_registry(input_value: str, action_name: str) -> tuple[bool, str]:
"""Validate registry format."""
if action_name == "docker-publish":
if input_value not in ["dockerhub", "github", "both"]:
return False, "Invalid registry value. Must be 'dockerhub', 'github', or 'both'"
elif input_value and not re.match(r"^[\w.-]+(:\d+)?$", input_value):
return False, f"Invalid registry format: {input_value}"
return True, ""
def _validate_file_path(input_value: str) -> tuple[bool, str]:
"""Validate file path format."""
if input_value and re.search(r"[;&|`$()]", input_value):
return False, f"Potential injection detected in file path: {input_value}"
if input_value and not re.match(r"^[a-zA-Z0-9._/,~-]+$", input_value):
return False, f"Invalid file path format: {input_value}"
return True, ""
def _validate_backoff_strategy(input_value: str) -> tuple[bool, str]:
"""Validate backoff strategy."""
if input_value not in ["linear", "exponential", "fixed"]:
return False, "Invalid backoff strategy. Must be 'linear', 'exponential', or 'fixed'"
return True, ""
def _validate_shell_type(input_value: str) -> tuple[bool, str]:
"""Validate shell type."""
if input_value not in ["bash", "sh"]:
return False, "Invalid shell type. Must be 'bash' or 'sh'"
return True, ""
def _validate_docker_image_name(input_value: str) -> tuple[bool, str]:
"""Validate docker image name format."""
if input_value and not re.match(
r"^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*$",
input_value,
):
return False, f"Invalid docker image name format: {input_value}"
return True, ""
def _validate_docker_tag(input_value: str) -> tuple[bool, str]:
"""Validate docker tag format."""
if input_value:
tags = [tag.strip() for tag in input_value.split(",")]
for tag in tags:
if not re.match(r"^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$", tag):
return False, f"Invalid docker tag format: {tag}"
return True, ""
def _validate_docker_password(input_value: str) -> tuple[bool, str]:
"""Validate docker password."""
if input_value and len(input_value) < 8:
return False, "Docker password must be at least 8 characters long"
return True, ""
def _validate_go_version(input_value: str) -> tuple[bool, str]:
"""Validate Go version format."""
if input_value in ["stable", "latest"]:
return True, ""
if input_value and not re.match(r"^v?\d+\.\d+(\.\d+)?", input_value):
return False, f"Invalid Go version format: {input_value}"
return True, ""
def _validate_timeout_with_unit(input_value: str) -> tuple[bool, str]:
"""Validate timeout with unit format."""
if input_value and not re.match(r"^\d+[smh]$", input_value):
return False, "Invalid timeout format. Use format like '5m', '300s', or '1h'"
return True, ""
def _validate_linter_list(input_value: str) -> tuple[bool, str]:
"""Validate linter list format."""
if input_value and re.search(r",\s+", input_value):
return False, "Invalid linter list format. Use comma-separated values without spaces"
return True, ""
def _validate_version_types(input_value: str) -> tuple[bool, str]:
"""Validate semantic/calver/flexible version formats."""
if input_value.lower() == "latest":
return True, ""
if input_value.startswith("v"):
return False, f"Version should not start with 'v': {input_value}"
if not re.match(r"^\d+\.\d+(\.\d+)?", input_value):
return False, f"Invalid version format: {input_value}"
return True, ""
def _validate_file_pattern(input_value: str) -> tuple[bool, str]:
"""Validate file pattern format."""
if input_value and ("../" in input_value or "\\..\\" in input_value):
return False, f"Path traversal not allowed in file patterns: {input_value}"
if input_value and input_value.startswith("/"):
return False, f"Absolute paths not allowed in file patterns: {input_value}"
if input_value and re.search(r"[;&|`$()]", input_value):
return False, f"Potential injection detected in file pattern: {input_value}"
return True, ""
def _validate_report_format(input_value: str) -> tuple[bool, str]:
"""Validate report format."""
if input_value not in ["json", "sarif"]:
return False, "Invalid report format. Must be 'json' or 'sarif'"
return True, ""
def _validate_plugin_list(input_value: str) -> tuple[bool, str]:
"""Validate plugin list format."""
if input_value and re.search(r"[;&|`$()]", input_value):
return False, f"Potential injection detected in plugin list: {input_value}"
return True, ""
def _validate_prefix(input_value: str) -> tuple[bool, str]:
"""Validate prefix format."""
if input_value and re.search(r"[;&|`$()]", input_value):
return False, f"Potential injection detected in prefix: {input_value}"
return True, ""
def _validate_terraform_version(input_value: str) -> tuple[bool, str]:
"""Validate terraform version format."""
if input_value and input_value.lower() == "latest":
return True, ""
if input_value and input_value.startswith("v"):
return False, f"Terraform version should not start with 'v': {input_value}"
if input_value and not re.match(r"^\d+\.\d+(\.\d+)?", input_value):
return False, f"Invalid terraform version format: {input_value}"
return True, ""
def _validate_php_extensions(input_value: str) -> tuple[bool, str]:
"""Validate PHP extensions format."""
if input_value and re.search(r"[;&|`$()@#]", input_value):
return False, f"Potential injection detected in PHP extensions: {input_value}"
if input_value and not re.match(r"^[a-zA-Z0-9_,\s]+$", input_value):
return False, f"Invalid PHP extensions format: {input_value}"
return True, ""
def _validate_coverage_driver(input_value: str) -> tuple[bool, str]:
"""Validate coverage driver."""
if input_value not in ["none", "xdebug", "pcov", "xdebug3"]:
return False, "Invalid coverage driver. Must be 'none', 'xdebug', 'pcov', or 'xdebug3'"
return True, ""
# Validation registry mapping types to functions and their argument requirements
VALIDATION_REGISTRY = {
"boolean": (_validate_boolean, "input_name"),
"docker_architectures": (_validate_docker_architectures, "value_only"),
"registry": (_validate_registry, "action_name"),
"file_path": (_validate_file_path, "value_only"),
"backoff_strategy": (_validate_backoff_strategy, "value_only"),
"shell_type": (_validate_shell_type, "value_only"),
"docker_image_name": (_validate_docker_image_name, "value_only"),
"docker_tag": (_validate_docker_tag, "value_only"),
"docker_password": (_validate_docker_password, "value_only"),
"go_version": (_validate_go_version, "value_only"),
"timeout_with_unit": (_validate_timeout_with_unit, "value_only"),
"linter_list": (_validate_linter_list, "value_only"),
"semantic_version": (_validate_version_types, "value_only"),
"calver_version": (_validate_version_types, "value_only"),
"flexible_version": (_validate_version_types, "value_only"),
"file_pattern": (_validate_file_pattern, "value_only"),
"report_format": (_validate_report_format, "value_only"),
"plugin_list": (_validate_plugin_list, "value_only"),
"prefix": (_validate_prefix, "value_only"),
"terraform_version": (_validate_terraform_version, "value_only"),
"php_extensions": (_validate_php_extensions, "value_only"),
"coverage_driver": (_validate_coverage_driver, "value_only"),
}
def _load_validation_rules(action_dir: str) -> tuple[dict, bool]:
"""Load validation rules for an action."""
action_name = Path(action_dir).name
script_dir = Path(__file__).resolve().parent
project_root = script_dir.parent.parent
rules_file = project_root / "validate-inputs" / "rules" / f"{action_name}.yml"
if not rules_file.exists():
return {}, False
try:
with Path(rules_file).open(encoding="utf-8") as f:
return yaml.safe_load(f), True
except Exception as e: # pylint: disable=broad-exception-caught
print(f"Warning: Could not load centralized rules for {action_name}: {e}", file=sys.stderr)
return {}, False
def _get_validation_type(input_name: str, rules_data: dict) -> str | None:
"""Get validation type for an input from rules."""
conventions = rules_data.get("conventions", {})
overrides = rules_data.get("overrides", {})
# Check overrides first, then conventions
if input_name in overrides:
return overrides[input_name]
if input_name in conventions:
return conventions[input_name]
return None
def _validate_with_centralized_rules(
input_name: str,
input_value: str,
action_dir: str,
validator: ActionValidator,
) -> tuple[bool, str, bool]:
"""Validate input using centralized validation rules."""
rules_data, rules_loaded = _load_validation_rules(action_dir)
if not rules_loaded:
return True, "", False
action_name = Path(action_dir).name
required_inputs = rules_data.get("required_inputs", [])
# Check if input is required and empty
if input_name in required_inputs and (not input_value or input_value.strip() == ""):
return False, f"Required input '{input_name}' cannot be empty", True
validation_type = _get_validation_type(input_name, rules_data)
if validation_type is None:
return True, "", False
# Handle special validator-based types
if validation_type == "github_token":
token_valid, token_error = validator.validate_github_token(input_value, action_dir)
return token_valid, token_error, True
if validation_type == "namespace_with_lookahead":
ns_valid, ns_error = validator.validate_namespace_with_lookahead(input_value)
return ns_valid, ns_error, True
# Use registry for other validation types
if validation_type in VALIDATION_REGISTRY:
validate_func, arg_type = VALIDATION_REGISTRY[validation_type]
if arg_type == "value_only":
is_valid, error_msg = validate_func(input_value)
elif arg_type == "input_name":
is_valid, error_msg = validate_func(input_value, input_name)
elif arg_type == "action_name":
is_valid, error_msg = validate_func(input_value, action_name)
else:
return False, f"Unknown validation argument type: {arg_type}", True
return is_valid, error_msg, True
return True, "", True
def _validate_special_inputs(
input_name: str,
input_value: str,
action_dir: str,
validator: ActionValidator,
) -> tuple[bool, str, bool]:
"""Handle special input validation cases."""
action_name = Path(action_dir).name
if action_name == "docker-build":
is_valid, error_message = _validate_docker_build_input(input_name, input_value)
return is_valid, error_message, True
if input_name == "token" and action_name in [
"csharp-publish",
"eslint-fix",
"pr-lint",
"pre-commit",
]:
# Special handling for GitHub tokens
token_valid, token_error = validator.validate_github_token(input_value, action_dir)
return token_valid, token_error, True
if input_name == "namespace" and action_name == "csharp-publish":
# Special handling for namespace with lookahead
ns_valid, ns_error = validator.validate_namespace_with_lookahead(input_value)
return ns_valid, ns_error, True
return True, "", False
def _validate_with_patterns(
input_name: str,
input_value: str,
patterns: dict,
validator: ActionValidator,
) -> tuple[bool, str, bool]:
"""Validate input using extracted patterns."""
if input_name not in patterns:
return True, "", False
for pattern in patterns[input_name]:
pattern_valid, pattern_error = validator.validate_input_pattern(
input_value,
pattern,
)
if not pattern_valid:
return False, pattern_error, True
return True, "", True
def _handle_test_mode(expected_result: str, *, is_valid: bool) -> None:
"""Handle test mode output and exit."""
if (expected_result == "success" and is_valid) or (
expected_result == "failure" and not is_valid
):
sys.exit(0) # Test expectation met
sys.exit(1) # Test expectation not met
def _handle_validation_mode(*, is_valid: bool, error_message: str) -> None:
"""Handle validation mode output and exit."""
if is_valid:
print("VALID")
sys.exit(0)
print(f"INVALID: {error_message}")
sys.exit(1)
def _handle_property_mode(args: dict) -> None:
"""Handle property checking mode."""
result = get_input_property(args["action_file"], args["input_name"], args["property"])
print(result)
def _handle_inputs_mode(args: dict) -> None:
"""Handle inputs listing mode."""
inputs = get_action_inputs(args["action_file"])
for input_name in inputs:
print(input_name)
def _handle_outputs_mode(args: dict) -> None:
"""Handle outputs listing mode."""
outputs = get_action_outputs(args["action_file"])
for output_name in outputs:
print(output_name)
def _handle_name_mode(args: dict) -> None:
"""Handle name getting mode."""
name = get_action_name(args["action_file"])
print(name)
def _perform_validation_steps(args: dict) -> tuple[bool, str]:
"""Perform all validation steps and return result."""
# Resolve action file path
action_file = _resolve_action_file_path(args["action_dir"])
# Initialize validator and extract patterns
validator = ActionValidator()
patterns = extract_validation_patterns(action_file)
# Perform security validation (always performed)
security_valid, security_error = validator.validate_security_patterns(args["input_value"])
if not security_valid:
return False, security_error
# Perform input-specific validation
# Check centralized rules first
is_valid, error_message, has_validation = _validate_with_centralized_rules(
args["input_name"],
args["input_value"],
args["action_dir"],
validator,
)
# If no centralized validation, check special input cases
if not has_validation:
is_valid, error_message, has_validation = _validate_special_inputs(
args["input_name"],
args["input_value"],
args["action_dir"],
validator,
)
# If no special validation, try pattern-based validation
if not has_validation:
is_valid, error_message, has_validation = _validate_with_patterns(
args["input_name"],
args["input_value"],
patterns,
validator,
)
return is_valid, error_message
def _handle_validation_mode_main(args: dict) -> None:
"""Handle validation mode from main function."""
is_valid, error_message = _perform_validation_steps(args)
# Handle output based on mode
if args["expected_result"]:
_handle_test_mode(args["expected_result"], is_valid=is_valid)
_handle_validation_mode(is_valid=is_valid, error_message=error_message)
def main():
"""Command-line interface for the validation module."""
args = _parse_command_line_args()
# Dispatch to appropriate mode handler
mode_handlers = {
"property": _handle_property_mode,
"inputs": _handle_inputs_mode,
"outputs": _handle_outputs_mode,
"name": _handle_name_mode,
"validation": _handle_validation_mode_main,
}
if args["mode"] in mode_handlers:
mode_handlers[args["mode"]](args)
else:
print(f"Unknown mode: {args['mode']}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,241 @@
---
name: Test version-file-parser Integration
on:
workflow_dispatch:
push:
paths:
- 'version-file-parser/**'
- '_tests/integration/workflows/version-file-parser-test.yml'
jobs:
test-version-file-parser:
runs-on: ubuntu-latest
strategy:
matrix:
test-case:
- name: 'Node.js project'
language: 'node'
tool-versions-key: 'nodejs'
dockerfile-image: 'node'
expected-version: '18.0.0'
setup-files: |
echo "18.17.0" > .nvmrc
cat > package.json <<EOF
{
"name": "test-project",
"engines": { "node": ">=18.0.0" }
}
EOF
touch package-lock.json
- name: 'PHP project'
language: 'php'
tool-versions-key: 'php'
dockerfile-image: 'php'
expected-version: '8.1'
setup-files: |
cat > composer.json <<EOF
{
"require": { "php": "^8.1" }
}
EOF
- name: 'Python project'
language: 'python'
tool-versions-key: 'python'
dockerfile-image: 'python'
expected-version: '3.9'
setup-files: |
echo "3.9.0" > .python-version
cat > pyproject.toml <<EOF
[tool.poetry.dependencies]
python = "^3.9"
EOF
- name: 'Go project'
language: 'go'
tool-versions-key: 'golang'
dockerfile-image: 'golang'
expected-version: '1.21'
setup-files: |
cat > go.mod <<EOF
module test-project
go 1.21
EOF
- name: '.tool-versions file'
language: 'node'
tool-versions-key: 'nodejs'
dockerfile-image: 'node'
expected-version: '18.16.0'
setup-files: |
echo "nodejs 18.16.0" > .tool-versions
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Clean up test files from previous runs
run: |
rm -f .nvmrc package.json package-lock.json composer.json .python-version pyproject.toml go.mod .tool-versions
- name: Setup test files
run: ${{ matrix.test-case.setup-files }}
- name: Test version-file-parser
id: test-action
uses: ./version-file-parser
with:
language: ${{ matrix.test-case.language }}
tool-versions-key: ${{ matrix.test-case.tool-versions-key }}
dockerfile-image: ${{ matrix.test-case.dockerfile-image }}
default-version: '1.0.0'
- name: Validate outputs
run: |
echo "Test case: ${{ matrix.test-case.name }}"
echo "Expected version: ${{ matrix.test-case.expected-version }}"
echo "Detected version: ${{ steps.test-action.outputs.detected-version }}"
echo "Package manager: ${{ steps.test-action.outputs.package-manager }}"
# Validate that we got some version
if [[ -z "${{ steps.test-action.outputs.detected-version }}" ]]; then
echo "❌ ERROR: No version detected"
exit 1
fi
# Validate version format (basic semver check)
if ! echo "${{ steps.test-action.outputs.detected-version }}" | grep -E '^[0-9]+\.[0-9]+(\.[0-9]+)?'; then
echo "❌ ERROR: Invalid version format: ${{ steps.test-action.outputs.detected-version }}"
exit 1
fi
# Validate detected version matches expected version (not the fallback)
if [[ "${{ steps.test-action.outputs.detected-version }}" != "${{ matrix.test-case.expected-version }}" ]]; then
echo "❌ ERROR: Version mismatch"
echo "Expected: ${{ matrix.test-case.expected-version }}"
echo "Got: ${{ steps.test-action.outputs.detected-version }}"
exit 1
fi
echo "✅ Version validation passed"
# Skip external reference test in local/CI environment to avoid auth issues
- name: Test external reference (info only)
run: |
echo "External reference test would use: ivuorinen/actions/version-file-parser@main"
echo "Skipping to avoid authentication issues in local testing"
test-edge-cases:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Clean up test files from previous runs
run: |
rm -f .nvmrc package.json package-lock.json composer.json .python-version pyproject.toml go.mod .tool-versions
- name: Setup test files (package.json engines)
shell: bash
run: |
set -Eeuo pipefail
cat > package.json <<'EOF'
{
"name": "edge-case",
"engines": { "node": ">=18.0.0" }
}
EOF
echo "18.17.0" > .nvmrc
- name: Test version detection from existing files
id: existing-version
uses: ./version-file-parser
with:
language: 'node'
tool-versions-key: 'nodejs'
dockerfile-image: 'node'
default-version: '20.0.0'
- name: Validate existing version detection
run: |
# The action detects Node.js version from package.json engines field
# package.json >=18.0.0 is parsed as 18.0.0
# Note: .nvmrc exists but package.json takes precedence in this implementation
expected_version="18.0.0"
detected_version="${{ steps.existing-version.outputs.detected-version }}"
if [[ "$detected_version" != "$expected_version" ]]; then
echo "❌ ERROR: Version mismatch"
echo "Expected: $expected_version"
echo "Got: $detected_version"
exit 1
fi
echo "✅ Existing version detection works correctly"
- name: Clean up before invalid regex test
run: |
rm -f .nvmrc package.json package-lock.json
- name: Test with invalid regex
id: invalid-regex
uses: ./version-file-parser
with:
language: 'node'
tool-versions-key: 'nodejs'
dockerfile-image: 'node'
validation-regex: 'invalid[regex'
default-version: '18.0.0'
continue-on-error: true
- name: Validate regex error handling
run: |
echo "Testing regex error handling completed"
# Action should handle invalid regex gracefully
if [ "${{ steps.invalid-regex.outcome }}" != "failure" ]; then
echo "::error::Expected invalid-regex step to fail, but it was: ${{ steps.invalid-regex.outcome }}"
exit 1
fi
echo "✅ Invalid regex properly failed as expected"
test-dockerfile-parsing:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Clean up test files from previous runs
run: |
rm -f .nvmrc package.json package-lock.json composer.json .python-version pyproject.toml go.mod .tool-versions Dockerfile
- name: Create Dockerfile with Node.js
run: |
cat > Dockerfile <<EOF
FROM node:18.17.0-alpine
WORKDIR /app
COPY . .
EOF
- name: Test Dockerfile parsing
id: dockerfile-test
uses: ./version-file-parser
with:
language: 'node'
tool-versions-key: 'nodejs'
dockerfile-image: 'node'
- name: Validate Dockerfile parsing
run: |
expected_version="18.17.0"
detected_version="${{ steps.dockerfile-test.outputs.dockerfile-version }}"
echo "Expected version: $expected_version"
echo "Detected version: $detected_version"
if [[ "$detected_version" != "$expected_version" ]]; then
echo "❌ ERROR: Version mismatch"
echo "Expected: $expected_version"
echo "Got: $detected_version"
exit 1
fi
echo "✅ Dockerfile parsing successful"

757
_tests/run-tests.sh Executable file
View File

@@ -0,0 +1,757 @@
#!/usr/bin/env bash
# GitHub Actions Testing Framework - Main Test Runner
# Executes tests across all levels: unit, integration, and e2e
set -euo pipefail
# Script directory and test root
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_ROOT="$SCRIPT_DIR"
# Source framework utilities
# shellcheck source=_tests/framework/setup.sh
source "${TEST_ROOT}/framework/setup.sh"
# Configuration
DEFAULT_TEST_TYPE="all"
DEFAULT_ACTION_FILTER=""
PARALLEL_JOBS=4
COVERAGE_ENABLED=true
REPORT_FORMAT="console"
# Usage information
usage() {
cat <<EOF
GitHub Actions Testing Framework
Usage: $0 [OPTIONS] [ACTION_NAME...]
OPTIONS:
-t, --type TYPE Test type: unit, integration, e2e, all (default: all)
-a, --action ACTION Filter by specific action name
-j, --jobs JOBS Number of parallel jobs (default: 4)
-c, --coverage Enable coverage reporting (default: true)
--no-coverage Disable coverage reporting
-f, --format FORMAT Report format: console, json, junit, sarif (default: console)
-v, --verbose Enable verbose output
-h, --help Show this help message
EXAMPLES:
$0 # Run all tests for all actions
$0 -t unit # Run only unit tests
$0 -a node-setup # Test only node-setup action
$0 -t integration docker-build # Integration tests for docker-build
$0 --format json --coverage # Full tests with JSON output and coverage
$0 --format sarif # Generate SARIF report for security scanning
TEST TYPES:
unit - Fast unit tests for action validation and logic
integration - Integration tests using nektos/act or workflows
e2e - End-to-end tests with complete workflows
all - All test types (default)
EOF
}
# Parse command line arguments
parse_args() {
local test_type="$DEFAULT_TEST_TYPE"
local action_filter="$DEFAULT_ACTION_FILTER"
local actions=()
while [[ $# -gt 0 ]]; do
case $1 in
-t | --type)
if [[ $# -lt 2 ]]; then
echo "Error: $1 requires an argument" >&2
usage
exit 1
fi
test_type="$2"
shift 2
;;
-a | --action)
if [[ $# -lt 2 ]]; then
echo "Error: $1 requires an argument" >&2
usage
exit 1
fi
action_filter="$2"
shift 2
;;
-j | --jobs)
if [[ $# -lt 2 ]]; then
echo "Error: $1 requires an argument" >&2
usage
exit 1
fi
PARALLEL_JOBS="$2"
shift 2
;;
-c | --coverage)
COVERAGE_ENABLED=true
shift
;;
--no-coverage)
COVERAGE_ENABLED=false
shift
;;
-f | --format)
if [[ $# -lt 2 ]]; then
echo "Error: $1 requires an argument" >&2
usage
exit 1
fi
REPORT_FORMAT="$2"
shift 2
;;
-v | --verbose)
set -x
shift
;;
-h | --help)
usage
exit 0
;;
--)
shift
actions+=("$@")
break
;;
-*)
log_error "Unknown option: $1"
usage
exit 1
;;
*)
actions+=("$1")
shift
;;
esac
done
# Export for use in other functions
export TEST_TYPE="$test_type"
export ACTION_FILTER="$action_filter"
TARGET_ACTIONS=("${actions[@]+"${actions[@]}"}")
}
# Discover available actions
discover_actions() {
local actions=()
if [[ ${#TARGET_ACTIONS[@]} -gt 0 ]]; then
# Use provided actions
actions=("${TARGET_ACTIONS[@]}")
elif [[ -n $ACTION_FILTER ]]; then
# Filter by pattern
while IFS= read -r action_dir; do
local action_name
action_name=$(basename "$action_dir")
if [[ $action_name == *"$ACTION_FILTER"* ]]; then
actions+=("$action_name")
fi
done < <(find "${TEST_ROOT}/.." -mindepth 1 -maxdepth 1 -type d -name "*-*" | sort)
else
# All actions
while IFS= read -r action_dir; do
local action_name
action_name=$(basename "$action_dir")
actions+=("$action_name")
done < <(find "${TEST_ROOT}/.." -mindepth 1 -maxdepth 1 -type d -name "*-*" | sort)
fi
log_info "Discovered ${#actions[@]} actions to test: ${actions[*]}"
printf '%s\n' "${actions[@]}"
}
# Check if required tools are available
check_dependencies() {
# Check for ShellSpec
if ! command -v shellspec >/dev/null 2>&1; then
log_warning "ShellSpec not found, attempting to install..."
install_shellspec
fi
# Check for act (if running integration tests)
if [[ $TEST_TYPE == "integration" || $TEST_TYPE == "all" ]]; then
if ! command -v act >/dev/null 2>&1; then
log_warning "nektos/act not found, integration tests will be limited"
fi
fi
# Check for coverage tools (if enabled)
if [[ $COVERAGE_ENABLED == "true" ]]; then
if ! command -v kcov >/dev/null 2>&1; then
log_warning "kcov not found - coverage will use alternative methods"
fi
fi
log_success "Dependency check completed"
}
# Install ShellSpec if not available
install_shellspec() {
log_info "Installing ShellSpec testing framework..."
local shellspec_version="0.28.1"
local install_dir="${HOME}/.local"
# Download and install ShellSpec (download -> verify SHA256 -> extract -> install)
local tarball
tarball="$(mktemp /tmp/shellspec-XXXXXX.tar.gz)"
# Pinned SHA256 checksum for ShellSpec 0.28.1
# Source: https://github.com/shellspec/shellspec/archive/refs/tags/0.28.1.tar.gz
local checksum="351e7a63b8df47c07b022c19d21a167b85693f5eb549fa96e64f64844b680024"
# Ensure cleanup of the downloaded file
# Use ${tarball:-} to handle unbound variable when trap fires after function returns
cleanup() {
rm -f "${tarball:-}"
}
trap cleanup EXIT
log_info "Downloading ShellSpec ${shellspec_version} to ${tarball}..."
if ! curl -fsSL -o "$tarball" "https://github.com/shellspec/shellspec/archive/refs/tags/${shellspec_version}.tar.gz"; then
log_error "Failed to download ShellSpec ${shellspec_version}"
exit 1
fi
# Compute SHA256 in a portable way
local actual_sha
if command -v sha256sum >/dev/null 2>&1; then
actual_sha="$(sha256sum "$tarball" | awk '{print $1}')"
elif command -v shasum >/dev/null 2>&1; then
actual_sha="$(shasum -a 256 "$tarball" | awk '{print $1}')"
else
log_error "No SHA256 utility available (sha256sum or shasum required) to verify download"
exit 1
fi
if [[ "$actual_sha" != "$checksum" ]]; then
log_error "Checksum mismatch for ShellSpec ${shellspec_version} (expected ${checksum}, got ${actual_sha})"
exit 1
fi
log_info "Checksum verified for ShellSpec ${shellspec_version}, extracting..."
if ! tar -xzf "$tarball" -C /tmp/; then
log_error "Failed to extract ShellSpec archive"
exit 1
fi
if ! (cd "/tmp/shellspec-${shellspec_version}" && make install PREFIX="$install_dir"); then
log_error "ShellSpec make install failed"
exit 1
fi
# Add to PATH if not already there
if [[ ":$PATH:" != *":${install_dir}/bin:"* ]]; then
export PATH="${install_dir}/bin:$PATH"
# Append to shell rc only in non-CI environments
if [[ -z "${CI:-}" ]]; then
if ! grep -qxF "export PATH=\"${install_dir}/bin:\$PATH\"" ~/.bashrc 2>/dev/null; then
echo "export PATH=\"${install_dir}/bin:\$PATH\"" >>~/.bashrc
fi
fi
fi
if command -v shellspec >/dev/null 2>&1; then
log_success "ShellSpec installed successfully"
# Clear the trap now that we've succeeded to prevent unbound variable error on script exit
trap - EXIT
rm -f "$tarball"
else
log_error "Failed to install ShellSpec"
exit 1
fi
}
# Run unit tests
run_unit_tests() {
local actions=("$@")
local failed_tests=()
local passed_tests=()
log_info "Running unit tests for ${#actions[@]} actions..."
# Create test results directory
mkdir -p "${TEST_ROOT}/reports/unit"
for action in "${actions[@]}"; do
local unit_test_dir="${TEST_ROOT}/unit/${action}"
if [[ -d $unit_test_dir ]]; then
log_info "Running unit tests for: $action"
# Run ShellSpec tests
local test_result=0
local output_file="${TEST_ROOT}/reports/unit/${action}.txt"
# Run shellspec and capture both exit code and output
# Note: ShellSpec returns non-zero exit codes for warnings (101) and other conditions
# We need to check the actual output to determine if tests failed
# Pass action name relative to --default-path (_tests/unit) for proper spec_helper loading
(cd "$TEST_ROOT/.." && shellspec \
--format documentation \
"$action") >"$output_file" 2>&1 || true
# Parse the output to determine if tests actually failed
# Look for the summary line which shows "X examples, Y failures"
if grep -qE "[0-9]+ examples?, 0 failures?" "$output_file" && ! grep -q "Fatal error occurred" "$output_file"; then
log_success "Unit tests passed: $action"
passed_tests+=("$action")
else
# Check if there were actual failures (not just warnings)
if grep -qE "[0-9]+ examples?, [1-9][0-9]* failures?" "$output_file"; then
log_error "Unit tests failed: $action"
failed_tests+=("$action")
test_result=1
else
# No summary line found, treat as passed if no fatal errors
if ! grep -q "Fatal error occurred" "$output_file"; then
log_success "Unit tests passed: $action"
passed_tests+=("$action")
else
log_error "Unit tests failed: $action"
failed_tests+=("$action")
test_result=1
fi
fi
fi
# Show summary if verbose or on failure
if [[ $test_result -ne 0 || ${BASHOPTS:-} == *"xtrace"* || $- == *x* ]]; then
echo "--- Test output for $action ---"
cat "$output_file"
echo "--- End test output ---"
fi
else
log_warning "No unit tests found for: $action"
fi
done
# Report results
log_info "Unit test results:"
log_success " Passed: ${#passed_tests[@]} actions"
if [[ ${#failed_tests[@]} -gt 0 ]]; then
log_error " Failed: ${#failed_tests[@]} actions (${failed_tests[*]})"
return 1
fi
return 0
}
# Run integration tests using nektos/act
run_integration_tests() {
local actions=("$@")
local failed_tests=()
local passed_tests=()
log_info "Running integration tests for ${#actions[@]} actions..."
# Create test results directory
mkdir -p "${TEST_ROOT}/reports/integration"
for action in "${actions[@]}"; do
local workflow_file="${TEST_ROOT}/integration/workflows/${action}-test.yml"
if [[ -f $workflow_file ]]; then
log_info "Running integration test workflow for: $action"
# Run with act if available, otherwise skip
if command -v act >/dev/null 2>&1; then
local output_file="${TEST_ROOT}/reports/integration/${action}.txt"
# Create temp directory for artifacts
local artifacts_dir
artifacts_dir=$(mktemp -d) || exit 1
if act workflow_dispatch \
-W "$workflow_file" \
--container-architecture linux/amd64 \
--artifact-server-path "$artifacts_dir" \
-P ubuntu-latest=catthehacker/ubuntu:act-latest \
>"$output_file" 2>&1; then
log_success "Integration tests passed: $action"
passed_tests+=("$action")
else
log_error "Integration tests failed: $action"
failed_tests+=("$action")
# Show output on failure
echo "--- Integration test output for $action ---"
cat "$output_file"
echo "--- End integration test output ---"
fi
# Clean up artifacts directory
rm -rf "$artifacts_dir"
else
log_warning "Skipping integration test for $action (act not available)"
fi
else
log_warning "No integration test workflow found for: $action"
fi
done
# Report results
log_info "Integration test results:"
log_success " Passed: ${#passed_tests[@]} actions"
if [[ ${#failed_tests[@]} -gt 0 ]]; then
log_error " Failed: ${#failed_tests[@]} actions (${failed_tests[*]})"
return 1
fi
return 0
}
# Generate test coverage report
generate_coverage_report() {
if [[ $COVERAGE_ENABLED != "true" ]]; then
return 0
fi
log_info "Generating coverage report..."
local coverage_dir="${TEST_ROOT}/coverage"
mkdir -p "$coverage_dir"
# This is a simplified coverage implementation
# In practice, you'd integrate with kcov or similar tools
# Count tested vs total actions (count directories with action.yml files, excluding hidden/internal dirs and node_modules)
local project_root
project_root="$(cd "${TEST_ROOT}/.." && pwd)"
local total_actions
total_actions=$(find "$project_root" -mindepth 2 -maxdepth 2 -type f -name "action.yml" 2>/dev/null | wc -l | tr -d ' ')
# Count actions that have unit tests (by checking if validation.spec.sh exists)
local tested_actions
tested_actions=$(find "${TEST_ROOT}/unit" -mindepth 2 -maxdepth 2 -type f -name "validation.spec.sh" 2>/dev/null | wc -l | tr -d ' ')
local coverage_percent
if [[ $total_actions -gt 0 ]]; then
coverage_percent=$(((tested_actions * 100) / total_actions))
else
coverage_percent=0
fi
cat >"${coverage_dir}/summary.json" <<EOF
{
"total_actions": $total_actions,
"tested_actions": $tested_actions,
"coverage_percent": $coverage_percent,
"generated_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
}
EOF
log_success "Coverage report generated: ${coverage_percent}% ($tested_actions/$total_actions actions)"
}
# Generate test report
generate_test_report() {
log_info "Generating test report in format: $REPORT_FORMAT"
local report_dir="${TEST_ROOT}/reports"
mkdir -p "$report_dir"
case "$REPORT_FORMAT" in
"json")
generate_json_report
;;
"junit")
log_warning "JUnit report format not yet implemented, using JSON instead"
generate_json_report
;;
"sarif")
generate_sarif_report
;;
"console" | *)
generate_console_report
;;
esac
}
# Generate JSON test report
generate_json_report() {
local report_file="${TEST_ROOT}/reports/test-results.json"
cat >"$report_file" <<EOF
{
"test_run": {
"timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"type": "$TEST_TYPE",
"action_filter": "$ACTION_FILTER",
"parallel_jobs": $PARALLEL_JOBS,
"coverage_enabled": $COVERAGE_ENABLED
},
"results": {
"unit_tests": $(find "${TEST_ROOT}/reports/unit" -name "*.txt" 2>/dev/null | wc -l | tr -d ' '),
"integration_tests": $(find "${TEST_ROOT}/reports/integration" -name "*.txt" 2>/dev/null | wc -l | tr -d ' ')
}
}
EOF
log_success "JSON report generated: $report_file"
}
# Generate SARIF test report
generate_sarif_report() {
# Check for jq availability
if ! command -v jq >/dev/null 2>&1; then
log_warning "jq not found, skipping SARIF report generation"
return 0
fi
local report_file="${TEST_ROOT}/reports/test-results.sarif"
local run_id
run_id="github-actions-test-$(date +%s)"
local timestamp
timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
# Initialize SARIF structure using jq to ensure proper escaping
jq -n \
--arg run_id "$run_id" \
--arg timestamp "$timestamp" \
--arg test_type "$TEST_TYPE" \
'{
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"automationDetails": {
"id": $run_id
},
"tool": {
"driver": {
"name": "GitHub Actions Testing Framework",
"version": "1.0.0",
"informationUri": "https://github.com/ivuorinen/actions",
"rules": []
}
},
"results": [],
"invocations": [
{
"executionSuccessful": true,
"startTimeUtc": $timestamp,
"arguments": ["--type", $test_type, "--format", "sarif"]
}
]
}
]
}' >"$report_file"
# Parse test results and add SARIF findings
local results_array="[]"
local rules_array="[]"
# Process unit test failures
if [[ -d "${TEST_ROOT}/reports/unit" ]]; then
for test_file in "${TEST_ROOT}/reports/unit"/*.txt; do
if [[ -f "$test_file" ]]; then
local action_name
action_name=$(basename "$test_file" .txt)
# Check if test failed by looking for actual failures in the summary line
if grep -qE "[0-9]+ examples?, [1-9][0-9]* failures?" "$test_file" || grep -q "Fatal error occurred" "$test_file"; then
# Extract failure details
local failure_message
failure_message=$(grep -E "(Fatal error|failure|FAILED)" "$test_file" | head -1 || echo "Test failed")
# Add rule if not exists
if ! echo "$rules_array" | jq -e '.[] | select(.id == "test-failure")' >/dev/null 2>&1; then
rules_array=$(echo "$rules_array" | jq '. + [{
"id": "test-failure",
"name": "TestFailure",
"shortDescription": {"text": "Test execution failed"},
"fullDescription": {"text": "A unit or integration test failed during execution"},
"defaultConfiguration": {"level": "error"}
}]')
fi
# Add result using jq --arg to safely escape dynamic strings
results_array=$(echo "$results_array" | jq \
--arg failure_msg "$failure_message" \
--arg action_name "$action_name" \
'. + [{
"ruleId": "test-failure",
"level": "error",
"message": {"text": $failure_msg},
"locations": [{
"physicalLocation": {
"artifactLocation": {"uri": ($action_name + "/action.yml")},
"region": {"startLine": 1, "startColumn": 1}
}
}]
}]')
fi
fi
done
fi
# Process integration test failures similarly
if [[ -d "${TEST_ROOT}/reports/integration" ]]; then
for test_file in "${TEST_ROOT}/reports/integration"/*.txt; do
if [[ -f "$test_file" ]]; then
local action_name
action_name=$(basename "$test_file" .txt)
if grep -qE "FAILED|ERROR|error:" "$test_file"; then
local failure_message
failure_message=$(grep -E "(FAILED|ERROR|error:)" "$test_file" | head -1 || echo "Integration test failed")
# Add integration rule if not exists
if ! echo "$rules_array" | jq -e '.[] | select(.id == "integration-failure")' >/dev/null 2>&1; then
rules_array=$(echo "$rules_array" | jq '. + [{
"id": "integration-failure",
"name": "IntegrationFailure",
"shortDescription": {"text": "Integration test failed"},
"fullDescription": {"text": "An integration test failed during workflow execution"},
"defaultConfiguration": {"level": "warning"}
}]')
fi
# Add result using jq --arg to safely escape dynamic strings
results_array=$(echo "$results_array" | jq \
--arg failure_msg "$failure_message" \
--arg action_name "$action_name" \
'. + [{
"ruleId": "integration-failure",
"level": "warning",
"message": {"text": $failure_msg},
"locations": [{
"physicalLocation": {
"artifactLocation": {"uri": ($action_name + "/action.yml")},
"region": {"startLine": 1, "startColumn": 1}
}
}]
}]')
fi
fi
done
fi
# Update SARIF file with results and rules
local temp_file
temp_file=$(mktemp)
jq --argjson rules "$rules_array" --argjson results "$results_array" \
'.runs[0].tool.driver.rules = $rules | .runs[0].results = $results' \
"$report_file" >"$temp_file" && mv "$temp_file" "$report_file"
log_success "SARIF report generated: $report_file"
}
# Generate console test report
generate_console_report() {
echo ""
echo "========================================"
echo " GitHub Actions Test Framework Report"
echo "========================================"
echo "Test Type: $TEST_TYPE"
echo "Timestamp: $(date)"
echo "Coverage Enabled: $COVERAGE_ENABLED"
echo ""
if [[ -d "${TEST_ROOT}/reports/unit" ]]; then
local unit_tests
unit_tests=$(find "${TEST_ROOT}/reports/unit" -name "*.txt" 2>/dev/null | wc -l | tr -d ' ')
printf "%-25s %4s\n" "Unit Tests Run:" "$unit_tests"
fi
if [[ -d "${TEST_ROOT}/reports/integration" ]]; then
local integration_tests
integration_tests=$(find "${TEST_ROOT}/reports/integration" -name "*.txt" 2>/dev/null | wc -l | tr -d ' ')
printf "%-25s %4s\n" "Integration Tests Run:" "$integration_tests"
fi
if [[ -f "${TEST_ROOT}/coverage/summary.json" ]]; then
local coverage
coverage=$(jq -r '.coverage_percent' "${TEST_ROOT}/coverage/summary.json" 2>/dev/null || echo "N/A")
if [[ "$coverage" =~ ^[0-9]+$ ]]; then
printf "%-25s %4s%%\n" "Test Coverage:" "$coverage"
else
printf "%-25s %s\n" "Test Coverage:" "$coverage"
fi
fi
echo "========================================"
}
# Main test execution function
main() {
log_info "Starting GitHub Actions Testing Framework"
# Parse arguments
parse_args "$@"
# Initialize framework
init_testing_framework
# Check dependencies
check_dependencies
# Discover actions to test
local actions=()
while IFS= read -r action; do
actions+=("$action")
done < <(discover_actions)
if [[ ${#actions[@]} -eq 0 ]]; then
log_error "No actions found to test"
exit 1
fi
# Run tests based on type
local test_failed=false
case "$TEST_TYPE" in
"unit")
if ! run_unit_tests "${actions[@]}"; then
test_failed=true
fi
;;
"integration")
if ! run_integration_tests "${actions[@]}"; then
test_failed=true
fi
;;
"e2e")
log_warning "E2E tests not yet implemented"
;;
"all")
if ! run_unit_tests "${actions[@]}"; then
test_failed=true
fi
if ! run_integration_tests "${actions[@]}"; then
test_failed=true
fi
;;
*)
log_error "Unknown test type: $TEST_TYPE"
exit 1
;;
esac
# Generate coverage report
generate_coverage_report
# Generate test report
generate_test_report
# Final status
if [[ $test_failed == "true" ]]; then
log_error "Some tests failed"
exit 1
else
log_success "All tests passed!"
exit 0
fi
}
# Run main function if script is executed directly
if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then
main "$@"
fi

View File

@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Test docker image name regex fix for dots in validation_core.py."""
from pathlib import Path
import sys
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent))
# pylint: disable=wrong-import-position
from validation_core import ValidationCore
def test_docker_image_names_with_dots():
"""Test that docker image names with dots are accepted."""
validator = ValidationCore()
# Valid docker image names with dots (should pass)
valid_names = [
"my.app",
"app.with.dots",
"registry.example.com/myapp",
"docker.io/library/nginx",
"ghcr.io/owner/repo",
"gcr.io/project-id/image",
"quay.io/organization/app",
"my.registry.local/app.name",
"registry.example.com/namespace/app.name",
"harbor.example.com/project/image.name",
"nexus.company.local/docker/app",
]
print("Testing valid Docker image names with dots:")
for name in valid_names:
is_valid, error = validator.validate_docker_image_name(name)
status = "" if is_valid else ""
print(f" {status} {name:50s} {'PASS' if is_valid else f'FAIL: {error}'}")
assert is_valid, f"Should accept: {name} (got error: {error})"
# Invalid names (should fail)
invalid_names = [
"MyApp", # Uppercase
"my app", # Space
"-myapp", # Leading dash
"myapp-", # Trailing dash
"_myapp", # Leading underscore
]
print("\nTesting invalid Docker image names:")
for name in invalid_names:
is_valid, error = validator.validate_docker_image_name(name)
status = "" if not is_valid else ""
print(
f" {status} {name:50s} {'PASS (rejected)' if not is_valid else 'FAIL (should reject)'}"
)
assert not is_valid, f"Should reject: {name}"
print("\n✅ All tests passed!")
if __name__ == "__main__":
test_docker_image_names_with_dots()

882
_tests/shared/validation_core.py Executable file
View File

@@ -0,0 +1,882 @@
#!/usr/bin/env python3
"""
Shared validation core module for GitHub Actions.
This module consolidates all validation logic to eliminate duplication between
the framework validation and the centralized validator. It provides:
1. Standardized token patterns (resolved GitHub documentation discrepancies)
2. Common validation functions
3. Unified security validation
4. Centralized YAML parsing utilities
5. Command-line interface for ShellSpec test integration
This replaces inline Python code in ShellSpec tests and duplicate functions
across multiple files.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import re
import sys
from typing import Any
import yaml # pylint: disable=import-error
class ValidationCore:
"""Core validation functionality with standardized patterns and functions."""
# Standardized token patterns - resolved based on GitHub documentation
# Fine-grained tokens are 50-255 characters with underscores
TOKEN_PATTERNS = {
"classic": r"^gh[efpousr]_[a-zA-Z0-9]{36}$",
"fine_grained": r"^github_pat_[A-Za-z0-9_]{50,255}$", # 50-255 chars with underscores
"installation": r"^ghs_[a-zA-Z0-9]{36}$",
"npm_classic": r"^npm_[a-zA-Z0-9]{40,}$", # NPM classic tokens
}
# Injection detection pattern - characters commonly used in command injection
INJECTION_CHARS_PATTERN = r"[;&|`$()]"
# Security injection patterns
SECURITY_PATTERNS = [
r";\s*(rm|del|format|shutdown|reboot)",
r"&&\s*(rm|del|format|shutdown|reboot)",
r"\|\s*(rm|del|format|shutdown|reboot)",
r"`[^`]*`", # Command substitution
r"\$\([^)]*\)", # Command substitution
# Path traversal only dangerous when combined with commands
r"\.\./.*;\s*(rm|del|format|shutdown|reboot)",
r"\.\.\\+.*;\s*(rm|del|format|shutdown|reboot)", # Windows: ..\ or ..\\ patterns
]
def __init__(self):
"""Initialize the validation core."""
def validate_github_token(self, token: str, *, required: bool = False) -> tuple[bool, str]:
"""
Validate GitHub token format using standardized PCRE patterns.
Args:
token: The token to validate
required: Whether the token is required
Returns:
Tuple of (is_valid, error_message)
"""
if not token or token.strip() == "":
if required:
return False, "Token is required but not provided"
return True, ""
# Allow GitHub Actions expressions
if token == "${{ github.token }}" or (token.startswith("${{") and token.endswith("}}")):
return True, ""
# Allow environment variable references (e.g., $GITHUB_TOKEN)
if re.match(r"^\$[A-Za-z_][\w]*$", token):
return True, ""
# Check against standardized token patterns
for _token_type, pattern in self.TOKEN_PATTERNS.items():
if re.match(pattern, token):
return True, ""
return (
False,
"Invalid token format. Expected: gh[efpousr]_* (36 chars), "
"github_pat_[A-Za-z0-9_]* (50-255 chars), ghs_* (36 chars), or npm_* (40+ chars)",
)
def validate_namespace_with_lookahead(self, namespace: str) -> tuple[bool, str]:
"""
Validate namespace using lookahead pattern for .NET namespaces.
Args:
namespace: The namespace to validate
Returns:
Tuple of (is_valid, error_message)
"""
if not namespace or namespace.strip() == "":
return False, "Namespace cannot be empty"
# Pattern with lookahead ensures hyphens are only allowed when followed by alphanumeric
pattern = r"^[a-zA-Z0-9]([a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$"
if re.match(pattern, namespace):
return True, ""
return (
False,
"Invalid namespace format. Must be 1-39 characters, "
"alphanumeric and hyphens, no trailing hyphens",
)
def validate_security_patterns(
self,
input_value: str,
input_name: str = "",
) -> tuple[bool, str]:
"""
Check for common security injection patterns.
Args:
input_value: The value to validate
input_name: Name of the input (for context)
Returns:
Tuple of (is_valid, error_message)
"""
# Allow empty values for most inputs (they're often optional)
if not input_value or input_value.strip() == "":
return True, ""
for pattern in self.SECURITY_PATTERNS:
if re.search(pattern, input_value, re.IGNORECASE):
return (
False,
f"Potential security injection pattern detected in {input_name or 'input'}",
)
return True, ""
def validate_boolean(self, value: str, input_name: str) -> tuple[bool, str]:
"""Validate boolean input with intelligent fallback for misclassified inputs."""
# Handle empty values
if not value:
return True, ""
# Standard boolean values
if value.lower() in ["true", "false"]:
return True, ""
# Intelligent fallback for misclassified inputs
# If input name suggests it should accept paths/directories, validate as such
if any(
keyword in input_name.lower()
for keyword in ["directories", "directory", "path", "file"]
):
return self.validate_cache_directories(value)
return False, f"Input '{input_name}' must be 'true' or 'false'"
def validate_version_format(
self,
value: str,
*,
allow_v_prefix: bool = False,
) -> tuple[bool, str]:
"""Validate semantic version format."""
if value.lower() == "latest":
return True, ""
if not allow_v_prefix and value.startswith("v"):
return False, f"Version should not start with 'v': {value}"
value = value.removeprefix("v") # Remove v prefix for validation
# Split validation to reduce complexity
# Base version: major.minor.patch (or simpler forms)
base_pattern = r"^[\d]+(\.[\d]+)?(\.[\d]+)?$"
# Version with prerelease/build: major.minor.patch-prerelease+build
extended_pattern = r"^[\d]+(\.[\d]+)?(\.[\d]+)?[-+][0-9A-Za-z.-]+$"
if re.match(base_pattern, value) or re.match(extended_pattern, value):
return True, ""
return False, f"Invalid version format: {value}"
def validate_file_path(self, value: str, *, allow_traversal: bool = False) -> tuple[bool, str]:
"""Validate file path format."""
if not value:
return True, ""
# Check for injection patterns
if re.search(self.INJECTION_CHARS_PATTERN, value):
return False, f"Potential injection detected in file path: {value}"
# Check for path traversal (unless explicitly allowed)
if not allow_traversal and ("../" in value or "..\\" in value):
return False, f"Path traversal not allowed: {value}"
# Check for absolute paths (often not allowed)
if value.startswith("/") or (len(value) > 1 and value[1] == ":"):
return False, f"Absolute paths not allowed: {value}"
return True, ""
def validate_docker_image_name(self, value: str) -> tuple[bool, str]:
"""Validate docker image name format."""
if not value:
return True, ""
# Split validation into parts to reduce regex complexity
# Valid format: lowercase alphanumeric with separators (., _, __, -) and optional namespace
if not re.match(r"^[a-z0-9]", value):
return False, f"Invalid docker image name format: {value}"
if not re.match(r"^[a-z0-9._/-]+$", value):
return False, f"Invalid docker image name format: {value}"
# Check for invalid patterns
if value.endswith((".", "_", "-", "/")):
return False, f"Invalid docker image name format: {value}"
if "//" in value or ".." in value:
return False, f"Invalid docker image name format: {value}"
return True, ""
def validate_docker_tag(self, value: str) -> tuple[bool, str]:
"""Validate Docker tag format."""
if not value:
return True, ""
# Docker tags must be valid ASCII and may contain lowercase and uppercase letters,
# digits, underscores, periods and dashes. Cannot start with period or dash.
# Max length is 128 characters.
if len(value) > 128:
return False, f"Docker tag too long (max 128 characters): {value}"
if not re.match(r"^[a-zA-Z0-9_][a-zA-Z0-9._-]*$", value):
return False, f"Invalid docker tag format: {value}"
return True, ""
def validate_php_extensions(self, value: str) -> tuple[bool, str]:
"""Validate PHP extensions format."""
if not value:
return True, ""
if re.search(r"[;&|`$()@#]", value):
return False, f"Potential injection detected in PHP extensions: {value}"
if not re.match(r"^[a-zA-Z0-9_,\s]+$", value):
return False, f"Invalid PHP extensions format: {value}"
return True, ""
def validate_coverage_driver(self, value: str) -> tuple[bool, str]:
"""Validate coverage driver."""
if value not in ["none", "xdebug", "pcov", "xdebug3"]:
return False, "Invalid coverage driver. Must be 'none', 'xdebug', 'pcov', or 'xdebug3'"
return True, ""
def validate_numeric_range(self, value: str, min_val: int, max_val: int) -> tuple[bool, str]:
"""Validate numeric value within range."""
try:
num = int(value)
if min_val <= num <= max_val:
return True, ""
return False, f"Value must be between {min_val} and {max_val}, got {num}"
except ValueError:
return False, f"Invalid numeric value: {value}"
def validate_php_version(self, value: str) -> tuple[bool, str]:
"""Validate PHP version format (allows X.Y and X.Y.Z)."""
if not value:
return True, ""
# PHP versions can be X.Y or X.Y.Z format
if re.match(r"^[\d]+\.[\d]+(\.[\d]+)?$", value):
return True, ""
return False, f"Invalid PHP version format: {value}"
def validate_composer_version(self, value: str) -> tuple[bool, str]:
"""Validate Composer version (1 or 2)."""
if value in ["1", "2"]:
return True, ""
return False, f"Invalid Composer version. Must be '1' or '2', got '{value}'"
def validate_stability(self, value: str) -> tuple[bool, str]:
"""Validate Composer stability."""
valid_stabilities = ["stable", "RC", "beta", "alpha", "dev"]
if value in valid_stabilities:
return True, ""
return False, f"Invalid stability. Must be one of: {', '.join(valid_stabilities)}"
def validate_cache_directories(self, value: str) -> tuple[bool, str]:
"""Validate cache directories (comma-separated paths)."""
if not value:
return True, ""
# Split by comma and validate each directory
directories = [d.strip() for d in value.split(",")]
for directory in directories:
if not directory:
continue
# Basic path validation
if re.search(self.INJECTION_CHARS_PATTERN, directory):
return False, f"Potential injection detected in directory path: {directory}"
# Check for path traversal (both Unix and Windows)
if re.search(r"\.\.[/\\]", directory):
return False, f"Path traversal not allowed in directory: {directory}"
# Check for absolute paths
if directory.startswith("/") or (len(directory) > 1 and directory[1] == ":"):
return False, f"Absolute paths not allowed in directory: {directory}"
return True, ""
def validate_tools(self, value: str) -> tuple[bool, str]:
"""Validate Composer tools format (allows @ for stability flags like dev-master@dev)."""
if not value:
return True, ""
# Check for injection patterns (@ removed to allow Composer stability flags)
if re.search(self.INJECTION_CHARS_PATTERN, value):
return False, f"Potential injection detected in tools: {value}"
return True, ""
def validate_numeric_range_1_10(self, value: str) -> tuple[bool, str]:
"""Validate numeric value between 1 and 10."""
return self.validate_numeric_range(value, 1, 10)
def validate_enhanced_business_logic(
self,
action_name: str,
input_name: str,
value: str,
) -> tuple[bool | None, str]:
"""
Enhanced business logic validation for specific action/input combinations.
Returns (None, "") if no enhanced validation applies, otherwise returns validation result.
"""
if not value: # Empty values are generally allowed, except for specific cases
# Some inputs should not be empty even if they're optional
if action_name == "php-composer" and input_name in ["composer-version"]:
return False, f"Empty {input_name} is not allowed"
return None, ""
# PHP Composer specific validations
if action_name == "php-composer":
return self._validate_php_composer_business_logic(input_name, value)
# Prettier-check specific validations
if action_name == "prettier-check":
return self._validate_prettier_check_business_logic(input_name, value)
# Add more action-specific validations here as needed
return None, "" # No enhanced validation applies
def _validate_composer_version(self, value: str) -> tuple[bool, str]:
"""Validate composer version input."""
if value not in ["1", "2"]:
return False, f"Composer version must be '1' or '2', got '{value}'"
return True, ""
def _validate_stability(self, value: str) -> tuple[bool, str]:
"""Validate stability input."""
valid_stabilities = ["stable", "RC", "beta", "alpha", "dev"]
if value not in valid_stabilities:
return (
False,
f"Invalid stability '{value}'. Must be one of: {', '.join(valid_stabilities)}",
)
return True, ""
def _validate_php_version(self, value: str) -> tuple[bool, str]:
"""Validate PHP version input."""
if not re.match(r"^[\d]+\.[\d]+(\.[\d]+)?$", value):
return False, f"Invalid PHP version format: {value}"
try:
major, minor = value.split(".")[:2]
major_num, minor_num = int(major), int(minor)
if major_num < 7:
return False, f"PHP version {value} is too old (minimum 7.0)"
if major_num > 20:
return False, f"Invalid PHP version: {value}"
if minor_num < 0 or minor_num > 99:
return False, f"Invalid PHP version: {value}"
except (ValueError, IndexError):
return False, f"Invalid PHP version format: {value}"
return True, ""
def _validate_extensions(self, value: str) -> tuple[bool, str]:
"""Validate PHP extensions input."""
if re.search(r"[@#$&*(){}\[\]|\\]", value):
return False, f"Invalid characters in PHP extensions: {value}"
return True, ""
def _validate_tools(self, value: str) -> tuple[bool, str]:
"""Validate tools input (@ allowed for Composer stability flags like dev-master@dev)."""
if re.search(r"[#$&*(){}\[\]|\\]", value):
return False, f"Invalid characters in tools specification: {value}"
return True, ""
def _validate_args(self, value: str) -> tuple[bool, str]:
"""Validate args input."""
if re.search(self.INJECTION_CHARS_PATTERN, value):
return False, f"Potentially dangerous characters in args: {value}"
return True, ""
def _validate_php_composer_business_logic(
self,
input_name: str,
value: str,
) -> tuple[bool | None, str]:
"""Business logic validation specific to php-composer action."""
validators = {
"composer-version": self._validate_composer_version,
"stability": self._validate_stability,
"php": self._validate_php_version,
"extensions": self._validate_extensions,
"tools": self._validate_tools,
"args": self._validate_args,
}
if input_name in validators:
is_valid, error_msg = validators[input_name](value)
return is_valid, error_msg
return None, "" # No specific validation for this input
def _validate_file_pattern_security(self, value: str) -> tuple[bool, str]:
"""Validate file-pattern for security issues."""
if ".." in value:
return False, "Path traversal detected in file-pattern"
if value.startswith("/"):
return False, "Absolute path not allowed in file-pattern"
if "$" in value:
return False, "Shell expansion not allowed in file-pattern"
return True, ""
def _validate_plugins_security(self, value: str) -> tuple[bool, str]:
"""Validate plugins for security issues."""
if re.search(self.INJECTION_CHARS_PATTERN, value):
return False, "Potentially dangerous characters in plugins"
if re.search(r"\$\{.*\}", value):
return False, "Variable expansion not allowed in plugins"
if re.search(r"\$\(.*\)", value):
return False, "Command substitution not allowed in plugins"
return True, ""
def _validate_prettier_check_business_logic(
self,
input_name: str,
value: str,
) -> tuple[bool | None, str]:
"""Business logic validation specific to prettier-check action."""
# Handle prettier-version specially (accepts "latest" or semantic version)
if input_name == "prettier-version":
if value == "latest":
return True, ""
# Otherwise validate as semantic version
return None, "" # Let standard semantic version validation handle it
# Validate file-pattern for security issues
if input_name == "file-pattern":
return self._validate_file_pattern_security(value)
# Validate report-format enum
if input_name == "report-format":
if value == "":
return False, "report-format cannot be empty"
if value not in ["json", "sarif"]:
return False, f"Invalid report-format: {value}"
return True, ""
# Validate plugins for security issues
if input_name == "plugins":
return self._validate_plugins_security(value)
return None, "" # No specific validation for this input
class ActionFileParser:
"""Parser for GitHub Action YAML files."""
@staticmethod
def load_action_file(action_file: str) -> dict[str, Any]:
"""Load and parse an action.yml file."""
try:
with Path(action_file).open(encoding="utf-8") as f:
return yaml.safe_load(f)
except (OSError, yaml.YAMLError) as e:
msg = f"Failed to load action file {action_file}: {e}"
raise ValueError(msg) from e
@staticmethod
def get_action_name(action_file: str) -> str:
"""Get the action name from an action.yml file."""
try:
data = ActionFileParser.load_action_file(action_file)
return data.get("name", "Unknown")
except (OSError, ValueError, yaml.YAMLError, AttributeError):
return "Unknown"
@staticmethod
def get_action_inputs(action_file: str) -> list[str]:
"""Get all input names from an action.yml file."""
try:
data = ActionFileParser.load_action_file(action_file)
inputs = data.get("inputs", {})
return list(inputs.keys())
except (OSError, ValueError, yaml.YAMLError, AttributeError):
return []
@staticmethod
def get_action_outputs(action_file: str) -> list[str]:
"""Get all output names from an action.yml file."""
try:
data = ActionFileParser.load_action_file(action_file)
outputs = data.get("outputs", {})
return list(outputs.keys())
except (OSError, ValueError, yaml.YAMLError, AttributeError):
return []
@staticmethod
def _get_required_property(input_data: dict, property_name: str) -> str:
"""Get the required/optional property."""
is_required = input_data.get("required") in [True, "true"]
if property_name == "required":
return "required" if is_required else "optional"
return "optional" if not is_required else "required"
@staticmethod
def _get_default_property(input_data: dict) -> str:
"""Get the default property."""
default_value = input_data.get("default", "")
return str(default_value) if default_value else "no-default"
@staticmethod
def _get_description_property(input_data: dict) -> str:
"""Get the description property."""
description = input_data.get("description", "")
return description if description else "no-description"
@staticmethod
def _get_all_optional_property(inputs: dict) -> str:
"""Get the all_optional property (list of required inputs)."""
required_inputs = [k for k, v in inputs.items() if v.get("required") in [True, "true"]]
return "none" if not required_inputs else ",".join(required_inputs)
@staticmethod
def get_input_property(action_file: str, input_name: str, property_name: str) -> str:
"""
Get a property of an input from an action.yml file.
Args:
action_file: Path to the action.yml file
input_name: Name of the input to check
property_name: Property to check (required, optional, default, description,
all_optional)
Returns:
- For 'required': 'required' or 'optional'
- For 'optional': 'optional' or 'required'
- For 'default': the default value or 'no-default'
- For 'description': the description or 'no-description'
- For 'all_optional': 'none' if no required inputs, else comma-separated list
"""
try:
data = ActionFileParser.load_action_file(action_file)
inputs = data.get("inputs", {})
input_data = inputs.get(input_name, {})
property_handlers = {
"required": lambda: ActionFileParser._get_required_property(
input_data, property_name
),
"optional": lambda: ActionFileParser._get_required_property(
input_data, property_name
),
"default": lambda: ActionFileParser._get_default_property(input_data),
"description": lambda: ActionFileParser._get_description_property(input_data),
"all_optional": lambda: ActionFileParser._get_all_optional_property(inputs),
}
if property_name in property_handlers:
return property_handlers[property_name]()
return f"unknown-property-{property_name}"
except (OSError, ValueError, yaml.YAMLError, AttributeError, KeyError) as e:
return f"error: {e}"
def resolve_action_file_path(action_dir: str) -> str:
"""Resolve the path to the action.yml file."""
action_dir_path = Path(action_dir)
if not action_dir_path.is_absolute():
# If relative, assume we're in _tests/shared and actions are at ../../
script_dir = Path(__file__).resolve().parent
project_root = script_dir.parent.parent
return str(project_root / action_dir / "action.yml")
return f"{action_dir}/action.yml"
def _apply_validation_by_type(
validator: ValidationCore,
validation_type: str,
input_value: str,
input_name: str,
required_inputs: list,
) -> tuple[bool, str]:
"""Apply validation based on the validation type."""
validation_map = {
"github_token": lambda: validator.validate_github_token(
input_value, required=input_name in required_inputs
),
"namespace_with_lookahead": lambda: validator.validate_namespace_with_lookahead(
input_value,
),
"boolean": lambda: validator.validate_boolean(input_value, input_name),
"file_path": lambda: validator.validate_file_path(input_value),
"docker_image_name": lambda: validator.validate_docker_image_name(input_value),
"docker_tag": lambda: validator.validate_docker_tag(input_value),
"php_extensions": lambda: validator.validate_php_extensions(input_value),
"coverage_driver": lambda: validator.validate_coverage_driver(input_value),
"php_version": lambda: validator.validate_php_version(input_value),
"composer_version": lambda: validator.validate_composer_version(input_value),
"stability": lambda: validator.validate_stability(input_value),
"cache_directories": lambda: validator.validate_cache_directories(input_value),
"tools": lambda: validator.validate_tools(input_value),
"numeric_range_1_10": lambda: validator.validate_numeric_range_1_10(input_value),
}
# Handle version formats
if validation_type in ["semantic_version", "calver_version", "flexible_version"]:
return validator.validate_version_format(input_value)
if validation_type == "terraform_version":
return validator.validate_version_format(input_value, allow_v_prefix=True)
# Use validation map for other types
if validation_type in validation_map:
return validation_map[validation_type]()
return True, "" # Unknown validation type, assume valid
def _load_and_validate_rules(
rules_file: Path,
input_name: str,
input_value: str,
) -> tuple[str | None, dict, list]:
"""Load validation rules and perform basic validation."""
try:
with Path(rules_file).open(encoding="utf-8") as f:
rules_data = yaml.safe_load(f)
conventions = rules_data.get("conventions", {})
overrides = rules_data.get("overrides", {})
required_inputs = rules_data.get("required_inputs", [])
# Check if input is required and empty
if input_name in required_inputs and (not input_value or input_value.strip() == ""):
return None, {}, [] # Will cause error in caller
# Get validation type
validation_type = overrides.get(input_name, conventions.get(input_name))
return validation_type, rules_data, required_inputs
except (OSError, yaml.YAMLError, KeyError, AttributeError):
return None, {}, []
def validate_input(action_dir: str, input_name: str, input_value: str) -> tuple[bool | None, str]:
"""
Validate an input value for a specific action.
This is the main validation entry point that replaces the complex
validation logic in the original framework.
"""
validator = ValidationCore()
# Always perform security validation first
security_valid, security_error = validator.validate_security_patterns(input_value, input_name)
if not security_valid:
return False, security_error
# Get action name for business logic and rules
action_name = Path(action_dir).name
# Check enhanced business logic first (takes precedence over general rules)
enhanced_validation = validator.validate_enhanced_business_logic(
action_name,
input_name,
input_value,
)
if enhanced_validation[0] is not None: # If enhanced validation has an opinion
return enhanced_validation
# Load validation rules from action folder
script_dir = Path(__file__).resolve().parent
project_root = script_dir.parent.parent
rules_file = project_root / action_name / "rules.yml"
if rules_file.exists():
validation_type, _rules_data, required_inputs = _load_and_validate_rules(
rules_file,
input_name,
input_value,
)
# Check for required input error
if input_name in required_inputs and (not input_value or input_value.strip() == ""):
return False, f"Required input '{input_name}' cannot be empty"
if validation_type:
try:
return _apply_validation_by_type(
validator,
validation_type,
input_value,
input_name,
required_inputs,
)
except (ValueError, AttributeError, KeyError, TypeError) as e:
print(
f"Warning: Could not apply validation for {action_name}: {e}",
file=sys.stderr,
)
# If no specific validation found, the security check is sufficient
return True, ""
def _handle_legacy_interface():
"""Handle legacy CLI interface for backward compatibility."""
if len(sys.argv) == 5 and all(not arg.startswith("-") for arg in sys.argv[1:]):
action_dir, input_name, input_value, expected_result = sys.argv[1:5]
is_valid, error_msg = validate_input(action_dir, input_name, input_value)
actual_result = "success" if is_valid else "failure"
if actual_result == expected_result:
sys.exit(0)
else:
print(f"Expected {expected_result}, got {actual_result}: {error_msg}", file=sys.stderr)
sys.exit(1)
return False # Not legacy interface
def _create_argument_parser():
"""Create and configure the argument parser."""
parser = argparse.ArgumentParser(
description="Shared validation core for GitHub Actions",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Validate an input value
python3 validation_core.py --validate action-dir input-name input-value
# Get input property
python3 validation_core.py --property action.yml input-name required
# List inputs
python3 validation_core.py --inputs action.yml
# List outputs
python3 validation_core.py --outputs action.yml
# Get action name
python3 validation_core.py --name action.yml
""",
)
mode_group = parser.add_mutually_exclusive_group(required=True)
mode_group.add_argument(
"--validate",
nargs=3,
metavar=("ACTION_DIR", "INPUT_NAME", "INPUT_VALUE"),
help="Validate an input value",
)
mode_group.add_argument(
"--property",
nargs=3,
metavar=("ACTION_FILE", "INPUT_NAME", "PROPERTY"),
help="Get input property",
)
mode_group.add_argument("--inputs", metavar="ACTION_FILE", help="List action inputs")
mode_group.add_argument("--outputs", metavar="ACTION_FILE", help="List action outputs")
mode_group.add_argument("--name", metavar="ACTION_FILE", help="Get action name")
mode_group.add_argument(
"--validate-yaml",
metavar="YAML_FILE",
help="Validate YAML file syntax",
)
return parser
def _handle_validate_command(args):
"""Handle the validate command."""
action_dir, input_name, input_value = args.validate
is_valid, error_msg = validate_input(action_dir, input_name, input_value)
if is_valid:
sys.exit(0)
else:
print(f"INVALID: {error_msg}", file=sys.stderr)
sys.exit(1)
def _handle_property_command(args):
"""Handle the property command."""
action_file, input_name, property_name = args.property
result = ActionFileParser.get_input_property(action_file, input_name, property_name)
print(result)
def _handle_inputs_command(args):
"""Handle the inputs command."""
inputs = ActionFileParser.get_action_inputs(args.inputs)
for input_name in inputs:
print(input_name)
def _handle_outputs_command(args):
"""Handle the outputs command."""
outputs = ActionFileParser.get_action_outputs(args.outputs)
for output_name in outputs:
print(output_name)
def _handle_name_command(args):
"""Handle the name command."""
name = ActionFileParser.get_action_name(args.name)
print(name)
def _handle_validate_yaml_command(args):
"""Handle the validate-yaml command."""
try:
with Path(args.validate_yaml).open(encoding="utf-8") as f:
yaml.safe_load(f)
sys.exit(0)
except (OSError, yaml.YAMLError) as e:
print(f"Invalid YAML: {e}", file=sys.stderr)
sys.exit(1)
def _execute_command(args):
"""Execute the appropriate command based on arguments."""
command_handlers = {
"validate": _handle_validate_command,
"property": _handle_property_command,
"inputs": _handle_inputs_command,
"outputs": _handle_outputs_command,
"name": _handle_name_command,
"validate_yaml": _handle_validate_yaml_command,
}
for command, handler in command_handlers.items():
if getattr(args, command, None):
handler(args)
return
def main():
"""Command-line interface for validation core."""
# Handle legacy interface first
_handle_legacy_interface()
# Parse arguments and execute command
parser = _create_argument_parser()
args = parser.parse_args()
try:
_execute_command(args)
except (ValueError, OSError, AttributeError) as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,150 @@
#!/usr/bin/env shellspec
# Unit tests for ansible-lint-fix action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "ansible-lint-fix action"
ACTION_DIR="ansible-lint-fix"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts all GitHub token formats"
When call validate_input_python "ansible-lint-fix" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "accepts organization token"
When call validate_input_python "ansible-lint-fix" "token" "gho_123456789012345678901234567890123456"
The status should be success
End
It "accepts user token"
When call validate_input_python "ansible-lint-fix" "token" "ghu_123456789012345678901234567890123456"
The status should be success
End
It "accepts server token"
When call validate_input_python "ansible-lint-fix" "token" "ghs_123456789012345678901234567890123456"
The status should be success
End
It "accepts refresh token"
When call validate_input_python "ansible-lint-fix" "token" "ghr_123456789012345678901234567890123456"
The status should be success
End
End
Context "when validating email input"
It "accepts valid email"
When call validate_input_python "ansible-lint-fix" "email" "test@example.com"
The status should be success
End
It "rejects invalid email without @"
When call validate_input_python "ansible-lint-fix" "email" "testexample.com"
The status should be failure
End
It "rejects invalid email without domain"
When call validate_input_python "ansible-lint-fix" "email" "test@"
The status should be failure
End
End
Context "when validating username input"
It "accepts valid username"
When call validate_input_python "ansible-lint-fix" "username" "github-actions"
The status should be success
End
It "rejects semicolon injection"
When call validate_input_python "ansible-lint-fix" "username" "user;rm -rf /"
The status should be failure
End
It "rejects ampersand injection"
When call validate_input_python "ansible-lint-fix" "username" "user&&malicious"
The status should be failure
End
It "rejects pipe injection"
When call validate_input_python "ansible-lint-fix" "username" "user|dangerous"
The status should be failure
End
It "rejects overly long username"
When call validate_input_python "ansible-lint-fix" "username" "this-username-is-definitely-too-long-for-github-maximum-length-limit"
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts valid retry count"
When call validate_input_python "ansible-lint-fix" "max-retries" "5"
The status should be success
End
It "rejects zero retries"
When call validate_input_python "ansible-lint-fix" "max-retries" "0"
The status should be failure
End
It "rejects negative retries"
When call validate_input_python "ansible-lint-fix" "max-retries" "-1"
The status should be failure
End
It "rejects retries above limit"
When call validate_input_python "ansible-lint-fix" "max-retries" "15"
The status should be failure
End
It "rejects non-numeric retries"
When call validate_input_python "ansible-lint-fix" "max-retries" "invalid"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Ansible Lint and Fix"
End
It "defines expected inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "token"
The output should include "username"
The output should include "email"
The output should include "max-retries"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "files_changed"
The output should include "lint_status"
The output should include "sarif_path"
End
End
Context "when validating security"
It "rejects command injection in token"
When call validate_input_python "ansible-lint-fix" "token" "ghp_123;rm -rf /"
The status should be failure
End
It "rejects command injection in email"
When call validate_input_python "ansible-lint-fix" "email" "user@domain.com;rm -rf /"
The status should be failure
End
It "validates all inputs for injection patterns"
# Username injection testing already covered above
When call validate_input_python "ansible-lint-fix" "max-retries" "3;malicious"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs consistently"
When call test_action_outputs "$ACTION_DIR" "token" "ghp_123456789012345678901234567890123456" "username" "github-actions" "email" "test@example.com" "max-retries" "3"
The status should be success
The stderr should include "Testing action outputs for: ansible-lint-fix"
The stderr should include "Output test passed for: ansible-lint-fix"
End
End
End

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env shellspec
# Unit tests for biome-check action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "biome-check action"
ACTION_DIR="biome-check"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts personal access token"
When call validate_input_python "biome-check" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "accepts organization token"
When call validate_input_python "biome-check" "token" "gho_123456789012345678901234567890123456"
The status should be success
End
It "accepts user token"
When call validate_input_python "biome-check" "token" "ghu_123456789012345678901234567890123456"
The status should be success
End
It "accepts server token"
When call validate_input_python "biome-check" "token" "ghs_123456789012345678901234567890123456"
The status should be success
End
It "accepts refresh token"
When call validate_input_python "biome-check" "token" "ghr_123456789012345678901234567890123456"
The status should be success
End
End
Context "when validating email input"
It "accepts valid email"
When call validate_input_python "biome-check" "email" "test@example.com"
The status should be success
End
It "rejects invalid email without @"
When call validate_input_python "biome-check" "email" "testexample.com"
The status should be failure
End
It "rejects invalid email without domain"
When call validate_input_python "biome-check" "email" "test@"
The status should be failure
End
End
Context "when validating username input"
It "accepts valid username"
When call validate_input_python "biome-check" "username" "github-actions"
The status should be success
End
It "rejects semicolon injection"
When call validate_input_python "biome-check" "username" "user;rm -rf /"
The status should be failure
End
It "rejects ampersand injection"
When call validate_input_python "biome-check" "username" "user&&malicious"
The status should be failure
End
It "rejects pipe injection"
When call validate_input_python "biome-check" "username" "user|dangerous"
The status should be failure
End
It "rejects overly long username"
When call validate_input_python "biome-check" "username" "this-username-is-definitely-too-long-for-github-maximum-length-limit"
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts valid retry count"
When call validate_input_python "biome-check" "max-retries" "5"
The status should be success
End
It "rejects zero retries"
When call validate_input_python "biome-check" "max-retries" "0"
The status should be failure
End
It "rejects negative retries"
When call validate_input_python "biome-check" "max-retries" "-1"
The status should be failure
End
It "rejects retries above limit"
When call validate_input_python "biome-check" "max-retries" "15"
The status should be failure
End
It "rejects non-numeric retries"
When call validate_input_python "biome-check" "max-retries" "invalid"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Biome Check"
End
It "defines expected inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "token"
The output should include "username"
The output should include "email"
The output should include "max-retries"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "check_status"
The output should include "errors_count"
The output should include "warnings_count"
End
End
Context "when validating security"
It "rejects command injection in token"
When call validate_input_python "biome-check" "token" "ghp_123;rm -rf /"
The status should be failure
End
It "rejects command injection in email"
When call validate_input_python "biome-check" "email" "user@domain.com;rm -rf /"
The status should be failure
End
It "validates all inputs for injection patterns"
When call validate_input_python "biome-check" "max-retries" "3;malicious"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs consistently"
When call test_action_outputs "$ACTION_DIR" "token" "ghp_123456789012345678901234567890123456" "username" "github-actions" "email" "test@example.com" "max-retries" "3"
The status should be success
The stderr should include "Testing action outputs for: biome-check"
The stderr should include "Output test passed for: biome-check"
End
End
End

View File

@@ -0,0 +1,148 @@
#!/usr/bin/env shellspec
# Unit tests for biome-fix action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "biome-fix action"
ACTION_DIR="biome-fix"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts personal access token"
When call validate_input_python "biome-fix" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "accepts organization token"
When call validate_input_python "biome-fix" "token" "gho_123456789012345678901234567890123456"
The status should be success
End
It "accepts user token"
When call validate_input_python "biome-fix" "token" "ghu_123456789012345678901234567890123456"
The status should be success
End
It "accepts server token"
When call validate_input_python "biome-fix" "token" "ghs_123456789012345678901234567890123456"
The status should be success
End
It "accepts refresh token"
When call validate_input_python "biome-fix" "token" "ghr_123456789012345678901234567890123456"
The status should be success
End
End
Context "when validating email input"
It "accepts valid email"
When call validate_input_python "biome-fix" "email" "test@example.com"
The status should be success
End
It "rejects invalid email without @"
When call validate_input_python "biome-fix" "email" "testexample.com"
The status should be failure
End
It "rejects invalid email without domain"
When call validate_input_python "biome-fix" "email" "test@"
The status should be failure
End
End
Context "when validating username input"
It "accepts valid username"
When call validate_input_python "biome-fix" "username" "github-actions"
The status should be success
End
It "rejects semicolon injection"
When call validate_input_python "biome-fix" "username" "user;rm -rf /"
The status should be failure
End
It "rejects ampersand injection"
When call validate_input_python "biome-fix" "username" "user&&malicious"
The status should be failure
End
It "rejects pipe injection"
When call validate_input_python "biome-fix" "username" "user|dangerous"
The status should be failure
End
It "rejects overly long username"
When call validate_input_python "biome-fix" "username" "this-username-is-definitely-too-long-for-github-maximum-length-limit"
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts valid retry count"
When call validate_input_python "biome-fix" "max-retries" "5"
The status should be success
End
It "rejects zero retries"
When call validate_input_python "biome-fix" "max-retries" "0"
The status should be failure
End
It "rejects negative retries"
When call validate_input_python "biome-fix" "max-retries" "-1"
The status should be failure
End
It "rejects retries above limit"
When call validate_input_python "biome-fix" "max-retries" "15"
The status should be failure
End
It "rejects non-numeric retries"
When call validate_input_python "biome-fix" "max-retries" "invalid"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Biome Fix"
End
It "defines expected inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "token"
The output should include "username"
The output should include "email"
The output should include "max-retries"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "files_changed"
The output should include "fix_status"
End
End
Context "when validating security"
It "rejects command injection in token"
When call validate_input_python "biome-fix" "token" "ghp_123;rm -rf /"
The status should be failure
End
It "rejects command injection in email"
When call validate_input_python "biome-fix" "email" "user@domain.com;rm -rf /"
The status should be failure
End
It "validates all inputs for injection patterns"
When call validate_input_python "biome-fix" "max-retries" "3;malicious"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs consistently"
When call test_action_outputs "$ACTION_DIR" "token" "ghp_123456789012345678901234567890123456" "username" "github-actions" "email" "test@example.com" "max-retries" "3"
The status should be success
The stderr should include "Testing action outputs for: biome-fix"
The stderr should include "Output test passed for: biome-fix"
End
End
End

View File

@@ -0,0 +1,377 @@
#!/usr/bin/env bash
Describe "codeql-analysis validation"
Include "_tests/unit/spec_helper.sh"
Describe "language validation"
It "validates javascript language"
When call validate_input_python "codeql-analysis" "language" "javascript"
The status should be success
End
It "validates typescript language"
When call validate_input_python "codeql-analysis" "language" "typescript"
The status should be success
End
It "validates python language"
When call validate_input_python "codeql-analysis" "language" "python"
The status should be success
End
It "validates java language"
When call validate_input_python "codeql-analysis" "language" "java"
The status should be success
End
It "validates csharp language"
When call validate_input_python "codeql-analysis" "language" "csharp"
The status should be success
End
It "validates cpp language"
When call validate_input_python "codeql-analysis" "language" "cpp"
The status should be success
End
It "validates c language"
When call validate_input_python "codeql-analysis" "language" "c"
The status should be success
End
It "validates go language"
When call validate_input_python "codeql-analysis" "language" "go"
The status should be success
End
It "validates ruby language"
When call validate_input_python "codeql-analysis" "language" "ruby"
The status should be success
End
It "validates swift language"
When call validate_input_python "codeql-analysis" "language" "swift"
The status should be success
End
It "validates kotlin language"
When call validate_input_python "codeql-analysis" "language" "kotlin"
The status should be success
End
It "validates actions language"
When call validate_input_python "codeql-analysis" "language" "actions"
The status should be success
End
It "validates case insensitive languages"
When call validate_input_python "codeql-analysis" "language" "JavaScript"
The status should be success
End
It "rejects invalid language"
When call validate_input_python "codeql-analysis" "language" "invalid-lang"
The status should be failure
End
It "rejects empty language"
When call validate_input_python "codeql-analysis" "language" ""
The status should be failure
End
It "rejects unsupported language"
When call validate_input_python "codeql-analysis" "language" "rust"
The status should be failure
End
End
Describe "queries validation"
It "validates security-extended queries"
When call validate_input_python "codeql-analysis" "queries" "security-extended"
The status should be success
End
It "validates security-and-quality queries"
When call validate_input_python "codeql-analysis" "queries" "security-and-quality"
The status should be success
End
It "validates code-scanning queries"
When call validate_input_python "codeql-analysis" "queries" "code-scanning"
The status should be success
End
It "validates default queries"
When call validate_input_python "codeql-analysis" "queries" "default"
The status should be success
End
It "validates case insensitive queries"
When call validate_input_python "codeql-analysis" "queries" "Security-Extended"
The status should be success
End
It "validates custom query file with .ql extension"
When call validate_input_python "codeql-analysis" "queries" "custom-queries.ql"
The status should be success
End
It "validates custom query suite with .qls extension"
When call validate_input_python "codeql-analysis" "queries" "my-suite.qls"
The status should be success
End
It "validates custom query file with path"
When call validate_input_python "codeql-analysis" "queries" ".github/codeql/custom.ql"
The status should be success
End
It "rejects invalid query suite"
When call validate_input_python "codeql-analysis" "queries" "invalid-suite"
The status should be failure
End
It "rejects empty queries"
When call validate_input_python "codeql-analysis" "queries" ""
The status should be failure
End
End
Describe "category validation"
It "validates proper category format"
When call validate_input_python "codeql-analysis" "category" "/language:javascript"
The status should be success
End
It "validates custom category"
When call validate_input_python "codeql-analysis" "category" "/custom/analysis"
The status should be success
End
It "validates category with underscores"
When call validate_input_python "codeql-analysis" "category" "/my_custom_category"
The status should be success
End
It "validates category with hyphens"
When call validate_input_python "codeql-analysis" "category" "/my-custom-category"
The status should be success
End
It "validates category with colons"
When call validate_input_python "codeql-analysis" "category" "/language:python:custom"
The status should be success
End
It "validates empty category (optional)"
When call validate_input_python "codeql-analysis" "category" ""
The status should be success
End
It "rejects category without leading slash"
When call validate_input_python "codeql-analysis" "category" "language:javascript"
The status should be failure
End
It "rejects category with invalid characters"
When call validate_input_python "codeql-analysis" "category" "/language@javascript"
The status should be failure
End
It "rejects category with spaces"
When call validate_input_python "codeql-analysis" "category" "/language javascript"
The status should be failure
End
End
Describe "config-file validation"
It "validates valid config file path"
When call validate_input_python "codeql-analysis" "config-file" ".github/codeql/config.yml"
The status should be success
End
It "validates relative config file path"
When call validate_input_python "codeql-analysis" "config-file" "codeql-config.yml"
The status should be success
End
It "validates empty config file (optional)"
When call validate_input_python "codeql-analysis" "config-file" ""
The status should be success
End
It "rejects absolute path"
When call validate_input_python "codeql-analysis" "config-file" "/etc/config.yml"
The status should be failure
End
It "rejects path traversal"
When call validate_input_python "codeql-analysis" "config-file" "../config.yml"
The status should be failure
End
End
Describe "checkout-ref validation"
It "validates main branch"
When call validate_input_python "codeql-analysis" "checkout-ref" "main"
The status should be success
End
It "validates feature branch"
When call validate_input_python "codeql-analysis" "checkout-ref" "feature/security-updates"
The status should be success
End
It "validates commit SHA"
When call validate_input_python "codeql-analysis" "checkout-ref" "abc123def456"
The status should be success
End
It "validates tag"
When call validate_input_python "codeql-analysis" "checkout-ref" "v1.2.3"
The status should be success
End
It "validates empty checkout-ref (optional)"
When call validate_input_python "codeql-analysis" "checkout-ref" ""
The status should be success
End
End
Describe "token validation"
It "validates classic GitHub token"
When call validate_input_python "codeql-analysis" "token" "ghp_1234567890abcdef1234567890abcdef1234"
The status should be success
End
It "validates fine-grained token"
When call validate_input_python "codeql-analysis" "token" "github_pat_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
The status should be success
End
It "validates installation token"
When call validate_input_python "codeql-analysis" "token" "ghs_1234567890abcdef1234567890abcdef1234"
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "codeql-analysis" "token" "invalid-token"
The status should be failure
End
It "rejects empty token"
When call validate_input_python "codeql-analysis" "token" ""
The status should be failure
End
End
Describe "working-directory validation"
It "validates current directory"
When call validate_input_python "codeql-analysis" "working-directory" "."
The status should be success
End
It "validates relative directory"
When call validate_input_python "codeql-analysis" "working-directory" "src"
The status should be success
End
It "validates nested directory"
When call validate_input_python "codeql-analysis" "working-directory" "backend/src"
The status should be success
End
It "rejects absolute path"
When call validate_input_python "codeql-analysis" "working-directory" "/home/user/project"
The status should be failure
End
It "rejects path traversal"
When call validate_input_python "codeql-analysis" "working-directory" "../other-project"
The status should be failure
End
End
Describe "upload-results validation"
It "validates true value"
When call validate_input_python "codeql-analysis" "upload-results" "true"
The status should be success
End
It "validates false value"
When call validate_input_python "codeql-analysis" "upload-results" "false"
The status should be success
End
It "rejects uppercase TRUE"
When call validate_input_python "codeql-analysis" "upload-results" "TRUE"
The status should be failure
End
It "rejects uppercase FALSE"
When call validate_input_python "codeql-analysis" "upload-results" "FALSE"
The status should be failure
End
It "rejects invalid boolean"
When call validate_input_python "codeql-analysis" "upload-results" "yes"
The status should be failure
End
It "rejects empty value"
When call validate_input_python "codeql-analysis" "upload-results" ""
The status should be failure
End
End
Describe "complete action validation"
It "validates all required inputs with minimal config"
# Set up environment for the validation
export INPUT_ACTION_TYPE="codeql-analysis"
export INPUT_LANGUAGE="javascript"
When call uv run validate-inputs/validator.py
The status should be success
The stderr should include "All input validation checks passed"
End
It "validates all inputs with full config"
# Set up environment for the validation
export INPUT_ACTION_TYPE="codeql-analysis"
export INPUT_LANGUAGE="python"
export INPUT_QUERIES="security-extended"
export INPUT_CONFIG_FILE=".github/codeql/config.yml"
export INPUT_CATEGORY="/custom/python-analysis"
export INPUT_CHECKOUT_REF="main"
export INPUT_TOKEN="ghp_1234567890abcdef1234567890abcdef1234"
export INPUT_WORKING_DIRECTORY="backend"
export INPUT_UPLOAD_RESULTS="true"
When call uv run validate-inputs/validator.py
The status should be success
The stderr should include "All input validation checks passed"
End
It "fails validation with missing required language"
# Set up environment for the validation
export INPUT_ACTION_TYPE="codeql-analysis"
unset INPUT_LANGUAGE
When call uv run validate-inputs/validator.py
The status should be failure
The stderr should include "Required input 'language' is missing"
End
It "fails validation with invalid language and queries"
# Set up environment for the validation
export INPUT_ACTION_TYPE="codeql-analysis"
export INPUT_LANGUAGE="invalid-lang"
export INPUT_QUERIES="invalid-suite"
When call uv run validate-inputs/validator.py
The status should be failure
The stderr should include "Unsupported CodeQL language"
The stderr should include "Invalid CodeQL query suite"
End
End
End

View File

@@ -0,0 +1,168 @@
#!/usr/bin/env shellspec
# Unit tests for common-cache action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "common-cache action"
ACTION_DIR="common-cache"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating cache type input"
It "accepts npm cache type"
When call validate_input_python "common-cache" "type" "npm"
The status should be success
End
It "accepts composer cache type"
When call validate_input_python "common-cache" "type" "composer"
The status should be success
End
It "accepts go cache type"
When call validate_input_python "common-cache" "type" "go"
The status should be success
End
It "accepts pip cache type"
When call validate_input_python "common-cache" "type" "pip"
The status should be success
End
It "accepts maven cache type"
When call validate_input_python "common-cache" "type" "maven"
The status should be success
End
It "accepts gradle cache type"
When call validate_input_python "common-cache" "type" "gradle"
The status should be success
End
It "rejects empty cache type"
When call validate_input_python "common-cache" "type" ""
The status should be failure
End
It "rejects invalid cache type"
Pending "TODO: Implement enum validation for cache type"
When call validate_input_python "common-cache" "type" "invalid-type"
The status should be failure
End
End
Context "when validating paths input"
It "accepts single path"
When call validate_input_python "common-cache" "paths" "node_modules"
The status should be success
End
It "accepts multiple paths"
When call validate_input_python "common-cache" "paths" "node_modules,dist,build"
The status should be success
End
It "rejects empty paths"
When call validate_input_python "common-cache" "paths" ""
The status should be failure
End
It "rejects path traversal"
When call validate_input_python "common-cache" "paths" "../../../etc/passwd"
The status should be failure
End
It "rejects command injection in paths"
When call validate_input_python "common-cache" "paths" "node_modules;rm -rf /"
The status should be failure
End
End
Context "when validating key-prefix input"
It "accepts valid key prefix"
When call validate_input_python "common-cache" "key-prefix" "v2-build"
The status should be success
End
It "rejects command injection in key-prefix"
When call validate_input_python "common-cache" "key-prefix" "v2&&malicious"
The status should be failure
End
End
Context "when validating key-files input"
It "accepts single key file"
When call validate_input_python "common-cache" "key-files" "package.json"
The status should be success
End
It "accepts multiple key files"
When call validate_input_python "common-cache" "key-files" "package.json,package-lock.json,yarn.lock"
The status should be success
End
It "rejects path traversal in key-files"
When call validate_input_python "common-cache" "key-files" "../../../sensitive.json"
The status should be failure
End
End
Context "when validating restore-keys input"
It "accepts valid restore keys format"
When call validate_input_python "common-cache" "restore-keys" "Linux-npm-,Linux-"
The status should be success
End
It "rejects malicious restore keys"
When call validate_input_python "common-cache" "restore-keys" "Linux-npm-;rm -rf /"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Common Cache"
End
It "defines required inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "type"
The output should include "paths"
End
It "defines optional inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "key-prefix"
The output should include "key-files"
The output should include "restore-keys"
The output should include "env-vars"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "cache-hit"
The output should include "cache-key"
The output should include "cache-paths"
End
End
Context "when validating security"
It "rejects injection in all input types"
When call validate_input_python "common-cache" "type" "npm;malicious"
The status should be failure
End
It "validates environment variable names safely"
When call validate_input_python "common-cache" "env-vars" "NODE_ENV,CI"
The status should be success
End
It "rejects injection in environment variables"
When call validate_input_python "common-cache" "env-vars" "NODE_ENV;rm -rf /"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs consistently"
When call test_action_outputs "$ACTION_DIR" "type" "npm" "paths" "node_modules"
The status should be success
The stderr should include "Testing action outputs for: common-cache"
The stderr should include "Output test passed for: common-cache"
End
End
End

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env shellspec
# Unit tests for common-file-check action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "common-file-check action"
ACTION_DIR="common-file-check"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating file-pattern input"
It "accepts simple file pattern"
When call validate_input_python "common-file-check" "file-pattern" "package.json"
The status should be success
End
It "accepts glob pattern with wildcard"
When call validate_input_python "common-file-check" "file-pattern" "*.json"
The status should be success
End
It "accepts glob pattern with question mark"
When call validate_input_python "common-file-check" "file-pattern" "test?.js"
The status should be success
End
It "accepts nested path pattern"
When call validate_input_python "common-file-check" "file-pattern" "src/**/*.ts"
The status should be success
End
It "accepts pattern with braces"
When call validate_input_python "common-file-check" "file-pattern" "*.{js,ts}"
The status should be success
End
It "accepts pattern with brackets"
When call validate_input_python "common-file-check" "file-pattern" "[A-Z]*.txt"
The status should be success
End
It "rejects empty file pattern"
When call validate_input_python "common-file-check" "file-pattern" ""
The status should be failure
End
It "rejects path traversal"
When call validate_input_python "common-file-check" "file-pattern" "../../../etc/passwd"
The status should be failure
End
It "rejects command injection"
When call validate_input_python "common-file-check" "file-pattern" "*.json;rm -rf /"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Common File Check"
End
It "defines expected inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "file-pattern"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "found"
End
End
Context "when validating security"
It "validates glob patterns safely"
When call validate_input_python "common-file-check" "file-pattern" "**/*.{js,ts,json}"
The status should be success
End
It "rejects injection in glob patterns"
When call validate_input_python "common-file-check" "file-pattern" "*.js&&malicious"
The status should be failure
End
It "rejects pipe injection in patterns"
When call validate_input_python "common-file-check" "file-pattern" "*.js|dangerous"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs consistently"
When call test_action_outputs "$ACTION_DIR" "file-pattern" "*.json"
The status should be success
The stderr should include "Testing action outputs for: common-file-check"
The stderr should include "Output test passed for: common-file-check"
End
End
End

View File

@@ -0,0 +1,165 @@
#!/usr/bin/env shellspec
# Unit tests for common-retry action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "common-retry action"
ACTION_DIR="common-retry"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating max-retries input"
It "accepts minimum value (1)"
When call validate_input_python "common-retry" "max-retries" "1"
The status should be success
End
It "accepts maximum value (10)"
When call validate_input_python "common-retry" "max-retries" "10"
The status should be success
End
It "rejects below minimum"
When call validate_input_python "common-retry" "max-retries" "0"
The status should be failure
End
It "rejects above maximum"
When call validate_input_python "common-retry" "max-retries" "11"
The status should be failure
End
It "rejects non-numeric"
When call validate_input_python "common-retry" "max-retries" "invalid"
The status should be failure
End
End
Context "when validating retry-delay input"
It "accepts minimum value (1)"
When call validate_input_python "common-retry" "retry-delay" "1"
The status should be success
End
It "accepts maximum value (300)"
When call validate_input_python "common-retry" "retry-delay" "300"
The status should be success
End
It "rejects below minimum"
When call validate_input_python "common-retry" "retry-delay" "0"
The status should be failure
End
It "rejects above maximum"
When call validate_input_python "common-retry" "retry-delay" "301"
The status should be failure
End
End
Context "when validating backoff-strategy input"
It "accepts linear strategy"
When call validate_input_python "common-retry" "backoff-strategy" "linear"
The status should be success
End
It "accepts exponential strategy"
When call validate_input_python "common-retry" "backoff-strategy" "exponential"
The status should be success
End
It "accepts fixed strategy"
When call validate_input_python "common-retry" "backoff-strategy" "fixed"
The status should be success
End
It "rejects invalid strategy"
When call validate_input_python "common-retry" "backoff-strategy" "invalid"
The status should be failure
End
End
Context "when validating timeout input"
It "accepts minimum value (1)"
When call validate_input_python "common-retry" "timeout" "1"
The status should be success
End
It "accepts maximum value (3600)"
When call validate_input_python "common-retry" "timeout" "3600"
The status should be success
End
It "rejects below minimum"
When call validate_input_python "common-retry" "timeout" "0"
The status should be failure
End
It "rejects above maximum"
When call validate_input_python "common-retry" "timeout" "3601"
The status should be failure
End
End
Context "when validating working-directory input"
It "accepts current directory"
When call validate_input_python "common-retry" "working-directory" "."
The status should be success
End
It "accepts relative path"
When call validate_input_python "common-retry" "working-directory" "src/app"
The status should be success
End
It "rejects path traversal"
When call validate_input_python "common-retry" "working-directory" "../../../etc"
The status should be failure
End
End
Context "when validating shell input"
It "accepts bash shell"
When call validate_input_python "common-retry" "shell" "bash"
The status should be success
End
It "accepts sh shell"
When call validate_input_python "common-retry" "shell" "sh"
The status should be success
End
It "rejects zsh shell"
When call validate_input_python "common-retry" "shell" "zsh"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Common Retry"
End
End
Context "when validating security"
It "rejects command injection with semicolon"
When call validate_input_python "common-retry" "command" "value; rm -rf /"
The status should be failure
End
It "rejects command injection with ampersand"
When call validate_input_python "common-retry" "command" "value && malicious"
The status should be failure
End
It "accepts valid success codes"
When call validate_input_python "common-retry" "success-codes" "0,1,2"
The status should be success
End
It "rejects success codes with injection"
When call validate_input_python "common-retry" "success-codes" "0;rm -rf /"
The status should be failure
End
It "accepts valid retry codes"
When call validate_input_python "common-retry" "retry-codes" "1,126,127"
The status should be success
End
It "rejects retry codes with injection"
When call validate_input_python "common-retry" "retry-codes" "1;rm -rf /"
The status should be failure
End
End
End

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env shellspec
# Unit tests for compress-images action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "compress-images action"
ACTION_DIR="compress-images"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating inputs"
It "accepts valid quality setting"
# pick one of the defined quality inputs
inputs="$(get_action_inputs "$ACTION_FILE")"
QUALITY_INPUT=$(echo "$inputs" | grep -E '^(image-quality|png-quality)$' | head -n1)
[ -z "$QUALITY_INPUT" ] && Skip "No quality input found in action.yml"
When call validate_input_python "compress-images" "$QUALITY_INPUT" "80"
The status should be success
End
It "rejects invalid quality"
# pick one of the defined quality inputs
inputs="$(get_action_inputs "$ACTION_FILE")"
QUALITY_INPUT=$(echo "$inputs" | grep -E '^(image-quality|png-quality)$' | head -n1)
[ -z "$QUALITY_INPUT" ] && Skip "No quality input found in action.yml"
When call validate_input_python "compress-images" "$QUALITY_INPUT" "150"
The status should be failure
End
It "accepts valid path pattern"
# use the defined path-filter input
PATH_INPUT="ignore-paths"
When call validate_input_python "compress-images" "$PATH_INPUT" "assets/**/*.{jpg,png}"
The status should be success
End
It "rejects injection in path"
# use the defined path-filter input
PATH_INPUT="ignore-paths"
When call validate_input_python "compress-images" "$PATH_INPUT" "images;rm -rf /tmp"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should match pattern "*Compress*"
End
End
End

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env shellspec
# Unit tests for csharp-build action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "csharp-build action"
ACTION_DIR="csharp-build"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating dotnet-version input"
It "accepts valid dotnet version"
When call validate_input_python "csharp-build" "dotnet-version" "8.0"
The status should be success
End
It "accepts dotnet 6 LTS"
When call validate_input_python "csharp-build" "dotnet-version" "6.0"
The status should be success
End
It "rejects invalid version"
When call validate_input_python "csharp-build" "dotnet-version" "invalid"
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts valid max-retries"
When call validate_input_python "csharp-build" "max-retries" "3"
The status should be success
End
It "accepts minimum retries"
When call validate_input_python "csharp-build" "max-retries" "1"
The status should be success
End
It "rejects zero retries"
When call validate_input_python "csharp-build" "max-retries" "0"
The status should be failure
End
It "rejects non-numeric retries"
When call validate_input_python "csharp-build" "max-retries" "invalid"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should match pattern "*C#*"
End
It "defines expected inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "dotnet-version"
The output should include "max-retries"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "build_status"
The output should include "test_status"
The output should include "dotnet_version"
The output should include "artifacts_path"
The output should include "test_results_path"
End
End
Context "when testing outputs"
It "produces all expected outputs consistently"
When call test_action_outputs "$ACTION_DIR" "dotnet-version" "8.0" "max-retries" "3"
The status should be success
The stderr should include "Testing action outputs for: csharp-build"
The stderr should include "Output test passed for: csharp-build"
End
End
End

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env shellspec
# Unit tests for csharp-lint-check action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "csharp-lint-check action"
ACTION_DIR="csharp-lint-check"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating inputs"
It "accepts valid dotnet version"
When call validate_input_python "csharp-lint-check" "dotnet-version" "8.0"
The status should be success
End
It "accepts valid dotnet version format"
When call validate_input_python "csharp-lint-check" "dotnet-version" "8.0.100"
The status should be success
End
It "rejects injection"
When call validate_input_python "csharp-lint-check" "dotnet-version" "8.0;malicious"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should match pattern "*C#*"
End
End
End

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env shellspec
# Unit tests for csharp-publish action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "csharp-publish action"
ACTION_DIR="csharp-publish"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating inputs"
It "accepts valid dotnet version"
When call validate_input_python "csharp-publish" "dotnet-version" "8.0"
The status should be success
End
It "accepts valid namespace"
When call validate_input_python "csharp-publish" "namespace" "ivuorinen"
The status should be success
End
It "accepts namespace with hyphens in middle"
When call validate_input_python "csharp-publish" "namespace" "my-org-name"
The status should be success
End
It "rejects namespace ending with hyphen"
When call validate_input_python "csharp-publish" "namespace" "invalid-"
The status should be failure
End
It "accepts valid GitHub token"
When call validate_input_python "csharp-publish" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "rejects injection in namespace"
When call validate_input_python "csharp-publish" "namespace" "invalid;malicious"
The status should be failure
End
It "rejects injection in token"
When call validate_input_python "csharp-publish" "token" "token;rm -rf /"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should match pattern "*C#*"
End
End
End

View File

@@ -0,0 +1,218 @@
#!/usr/bin/env shellspec
# Unit tests for docker-build action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "docker-build action"
ACTION_DIR="docker-build"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating image-name input"
It "accepts valid image name"
When call validate_input_python "docker-build" "image-name" "myapp"
The status should be success
End
It "accepts image name with registry prefix"
When call validate_input_python "docker-build" "image-name" "registry.example.com/myapp"
The status should be success
End
It "rejects command injection in image name"
When call validate_input_python "docker-build" "image-name" "app; rm -rf /"
The status should be failure
End
End
Context "when validating tag input"
It "accepts valid tag format"
When call validate_input_python "docker-build" "tag" "v1.0.0"
The status should be success
End
It "accepts semantic version tag"
When call validate_input_python "docker-build" "tag" "1.2.3"
The status should be success
End
It "accepts latest tag"
When call validate_input_python "docker-build" "tag" "latest"
The status should be success
End
It "rejects invalid tag format"
When call validate_input_python "docker-build" "tag" "invalid_tag!"
The status should be failure
End
End
Context "when validating architectures input"
It "accepts valid architectures list"
When call validate_input_python "docker-build" "architectures" "linux/amd64,linux/arm64"
The status should be success
End
It "accepts single architecture"
When call validate_input_python "docker-build" "architectures" "linux/amd64"
The status should be success
End
It "accepts ARM variants"
When call validate_input_python "docker-build" "architectures" "linux/arm/v7,linux/arm/v6"
The status should be success
End
End
Context "when validating dockerfile input"
It "accepts valid dockerfile path"
When call validate_input_python "docker-build" "dockerfile" "Dockerfile"
The status should be success
End
It "accepts custom dockerfile path"
When call validate_input_python "docker-build" "dockerfile" "docker/Dockerfile.prod"
The status should be success
End
It "rejects malicious dockerfile path"
When call validate_input_python "docker-build" "dockerfile" "../../../etc/passwd"
The status should be failure
End
End
Context "when validating context input"
It "accepts valid build context"
When call validate_input_python "docker-build" "context" "."
The status should be success
End
It "accepts relative context path"
When call validate_input_python "docker-build" "context" "src/app"
The status should be success
End
It "accepts path traversal in context (no validation in action)"
When call validate_input_python "docker-build" "context" "../../../etc"
The status should be success
End
End
Context "when validating build-args input"
It "accepts valid build args format"
When call validate_input_python "docker-build" "build-args" "NODE_ENV=production,VERSION=1.0.0"
The status should be success
End
It "accepts empty build args"
When call validate_input_python "docker-build" "build-args" ""
The status should be success
End
It "rejects malicious build args"
When call validate_input_python "docker-build" "build-args" "ARG=\$(rm -rf /)"
The status should be failure
End
End
Context "when validating cache inputs"
It "accepts valid cache mode"
When call validate_input_python "docker-build" "cache-mode" "max"
The status should be success
End
It "accepts min cache mode"
When call validate_input_python "docker-build" "cache-mode" "min"
The status should be success
End
It "accepts inline cache mode"
When call validate_input_python "docker-build" "cache-mode" "inline"
The status should be success
End
It "rejects invalid cache mode"
When call validate_input_python "docker-build" "cache-mode" "invalid"
The status should be failure
End
It "accepts valid cache-from format"
When call validate_input_python "docker-build" "cache-from" "type=registry,ref=myapp:cache"
The status should be success
End
End
Context "when validating security features"
It "accepts scan-image boolean"
When call validate_input_python "docker-build" "scan-image" "true"
The status should be success
End
It "accepts sign-image boolean"
When call validate_input_python "docker-build" "sign-image" "false"
The status should be success
End
It "accepts valid SBOM format"
When call validate_input_python "docker-build" "sbom-format" "spdx-json"
The status should be success
End
It "accepts cyclonedx SBOM format"
When call validate_input_python "docker-build" "sbom-format" "cyclonedx-json"
The status should be success
End
It "rejects invalid SBOM format"
When call validate_input_python "docker-build" "sbom-format" "invalid-format"
The status should be failure
End
End
Context "when validating performance options"
It "accepts valid parallel builds number"
When call validate_input_python "docker-build" "parallel-builds" "4"
The status should be success
End
It "accepts auto parallel builds"
When call validate_input_python "docker-build" "parallel-builds" "0"
The status should be success
End
It "rejects negative parallel builds"
When call validate_input_python "docker-build" "parallel-builds" "-1"
The status should be failure
End
It "rejects non-numeric parallel builds"
When call validate_input_python "docker-build" "parallel-builds" "not-a-number"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
When call get_action_name "$ACTION_FILE"
The output should match pattern "*Docker*"
End
It "defines all required inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "tag"
End
It "defines all expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "image-digest"
The output should include "metadata"
The output should include "platforms"
The output should include "build-time"
End
End
Context "when validating security"
It "rejects injection in all Docker inputs"
When call validate_input_python "docker-build" "tag" "v1.0.0;rm -rf /"
The status should be failure
End
It "validates buildx version safely"
When call validate_input_python "docker-build" "buildx-version" "0.12.0"
The status should be success
End
It "rejects malicious buildx version"
When call validate_input_python "docker-build" "buildx-version" "0.12;malicious"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs consistently"
When call test_action_outputs "$ACTION_DIR" "tag" "v1.0.0" "dockerfile" "Dockerfile"
The status should be success
The stderr should include "Testing action outputs for: docker-build"
The stderr should include "Output test passed for: docker-build"
End
End
End

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env shellspec
# Unit tests for docker-publish-gh action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "docker-publish-gh action"
ACTION_DIR="docker-publish-gh"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating inputs"
It "accepts valid image name"
When call validate_input_python "docker-publish-gh" "image-name" "myapp"
The status should be success
End
It "accepts valid GitHub token"
When call validate_input_python "docker-publish-gh" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "accepts valid tags"
When call validate_input_python "docker-publish-gh" "tags" "v1.0.0,latest"
The status should be success
End
It "rejects injection in token"
When call validate_input_python "docker-publish-gh" "token" "ghp_123;malicious"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should match pattern "*Docker*"
End
End
End

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env shellspec
# Unit tests for docker-publish-hub action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "docker-publish-hub action"
ACTION_DIR="docker-publish-hub"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating inputs"
It "accepts valid image name"
When call validate_input_python "docker-publish-hub" "image-name" "myapp"
The status should be success
End
It "accepts valid username"
When call validate_input_python "docker-publish-hub" "username" "dockeruser"
The status should be success
End
It "accepts valid password"
When call validate_input_python "docker-publish-hub" "password" "secretpassword123"
The status should be success
End
It "accepts valid tags"
When call validate_input_python "docker-publish-hub" "tags" "v1.0.0,latest"
The status should be success
End
It "rejects injection in username"
When call validate_input_python "docker-publish-hub" "username" "user;malicious"
The status should be failure
End
It "rejects injection in password"
When call validate_input_python "docker-publish-hub" "password" "pass;rm -rf /"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should match pattern "*Docker*"
End
End
End

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env shellspec
# Unit tests for docker-publish action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "docker-publish action"
ACTION_DIR="docker-publish"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating inputs"
It "accepts valid registry"
When call validate_input_python "docker-publish" "registry" "dockerhub"
The status should be success
End
It "accepts github registry"
When call validate_input_python "docker-publish" "registry" "github"
The status should be success
End
It "accepts both registry"
When call validate_input_python "docker-publish" "registry" "both"
The status should be success
End
It "rejects empty registry input"
When call validate_input_python "docker-publish" "registry" ""
The status should be failure
End
It "accepts boolean values for nightly"
When call validate_input_python "docker-publish" "nightly" "true"
The status should be success
End
It "accepts valid platforms format"
When call validate_input_python "docker-publish" "platforms" "linux/amd64,linux/arm64"
The status should be success
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should match pattern "*Docker*"
End
End
End

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env shellspec
# Unit tests for dotnet-version-detect action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "dotnet-version-detect action"
ACTION_DIR="dotnet-version-detect"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating default-version input"
It "accepts valid dotnet version"
When call validate_input_python "dotnet-version-detect" "default-version" "8.0"
The status should be success
End
It "accepts full semantic version"
When call validate_input_python "dotnet-version-detect" "default-version" "8.0.0"
The status should be success
End
It "accepts dotnet 6 version"
When call validate_input_python "dotnet-version-detect" "default-version" "6.0.0"
The status should be success
End
It "accepts dotnet 7 version"
When call validate_input_python "dotnet-version-detect" "default-version" "7.0.0"
The status should be success
End
It "rejects invalid version format"
When call validate_input_python "dotnet-version-detect" "default-version" "invalid"
The status should be failure
End
It "rejects version with leading zeros"
When call validate_input_python "dotnet-version-detect" "default-version" "08.0.0"
The status should be failure
End
It "rejects unsupported version"
When call validate_input_python "dotnet-version-detect" "default-version" "2.0.0"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Dotnet Version Detect"
End
It "defines expected inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "default-version"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "dotnet-version"
End
End
Context "when validating security"
It "rejects injection in version"
When call validate_input_python "dotnet-version-detect" "default-version" "8.0;malicious"
The status should be failure
End
It "validates version security"
When call validate_input_python "dotnet-version-detect" "default-version" "8.0&&malicious"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs consistently"
When call test_action_outputs "$ACTION_DIR" "default-version" "8.0"
The status should be success
The stderr should include "Testing action outputs for: dotnet-version-detect"
The stderr should include "Output test passed for: dotnet-version-detect"
End
End
End

View File

@@ -0,0 +1,355 @@
#!/usr/bin/env shellspec
# Unit tests for eslint-check action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "eslint-check action"
ACTION_DIR="eslint-check"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating working-directory input"
It "accepts current directory"
When call validate_input_python "eslint-check" "working-directory" "."
The status should be success
End
It "accepts relative path"
When call validate_input_python "eslint-check" "working-directory" "src/frontend"
The status should be success
End
It "accepts nested directory"
When call validate_input_python "eslint-check" "working-directory" "packages/ui"
The status should be success
End
It "rejects path traversal"
When call validate_input_python "eslint-check" "working-directory" "../../../etc/passwd"
The status should be failure
End
It "rejects absolute paths"
When call validate_input_python "eslint-check" "working-directory" "/etc/passwd"
The status should be failure
End
It "rejects injection attempts"
When call validate_input_python "eslint-check" "working-directory" "src; rm -rf /"
The status should be failure
End
End
Context "when validating eslint-version input"
It "accepts latest version"
When call validate_input_python "eslint-check" "eslint-version" "latest"
The status should be success
End
It "accepts semantic version"
When call validate_input_python "eslint-check" "eslint-version" "8.57.0"
The status should be success
End
It "accepts version with prerelease"
When call validate_input_python "eslint-check" "eslint-version" "9.0.0-alpha.0"
The status should be success
End
It "accepts older stable version"
When call validate_input_python "eslint-check" "eslint-version" "7.32.0"
The status should be success
End
It "rejects invalid version format"
When call validate_input_python "eslint-check" "eslint-version" "8.57"
The status should be failure
End
It "rejects version with letters"
When call validate_input_python "eslint-check" "eslint-version" "8.57.0a"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "eslint-check" "eslint-version" ""
The status should be failure
End
End
Context "when validating config-file input"
It "accepts default eslintrc"
When call validate_input_python "eslint-check" "config-file" ".eslintrc"
The status should be success
End
It "accepts eslintrc.json"
When call validate_input_python "eslint-check" "config-file" ".eslintrc.json"
The status should be success
End
It "accepts eslint.config.js"
When call validate_input_python "eslint-check" "config-file" "eslint.config.js"
The status should be success
End
It "accepts relative path config"
When call validate_input_python "eslint-check" "config-file" "config/eslint.json"
The status should be success
End
It "rejects path traversal"
When call validate_input_python "eslint-check" "config-file" "../../../malicious.js"
The status should be failure
End
It "rejects injection in config path"
When call validate_input_python "eslint-check" "config-file" "config.js;rm -rf /"
The status should be failure
End
End
Context "when validating ignore-file input"
It "accepts default eslintignore"
When call validate_input_python "eslint-check" "ignore-file" ".eslintignore"
The status should be success
End
It "accepts custom ignore file"
When call validate_input_python "eslint-check" "ignore-file" "eslint-ignore.txt"
The status should be success
End
It "accepts relative path ignore file"
When call validate_input_python "eslint-check" "ignore-file" "config/.eslintignore"
The status should be success
End
It "rejects path traversal"
When call validate_input_python "eslint-check" "ignore-file" "../../sensitive.txt"
The status should be failure
End
End
Context "when validating file-extensions input"
It "accepts default extensions"
When call validate_input_python "eslint-check" "file-extensions" ".js,.jsx,.ts,.tsx"
The status should be success
End
It "accepts single extension"
When call validate_input_python "eslint-check" "file-extensions" ".js"
The status should be success
End
It "accepts TypeScript extensions only"
When call validate_input_python "eslint-check" "file-extensions" ".ts,.tsx"
The status should be success
End
It "accepts Vue and JavaScript extensions"
When call validate_input_python "eslint-check" "file-extensions" ".js,.vue,.ts"
The status should be success
End
It "rejects extensions without dots"
When call validate_input_python "eslint-check" "file-extensions" "js,ts"
The status should be failure
End
It "rejects invalid extension format"
When call validate_input_python "eslint-check" "file-extensions" ".js;.ts"
The status should be failure
End
It "rejects extensions with special characters"
When call validate_input_python "eslint-check" "file-extensions" ".js,.t$"
The status should be failure
End
End
Context "when validating boolean inputs"
It "accepts cache as true"
When call validate_input_python "eslint-check" "cache" "true"
The status should be success
End
It "accepts cache as false"
When call validate_input_python "eslint-check" "cache" "false"
The status should be success
End
It "accepts fail-on-error as true"
When call validate_input_python "eslint-check" "fail-on-error" "true"
The status should be success
End
It "accepts fail-on-error as false"
When call validate_input_python "eslint-check" "fail-on-error" "false"
The status should be success
End
It "rejects invalid boolean value"
When call validate_input_python "eslint-check" "cache" "maybe"
The status should be failure
End
It "rejects numeric boolean"
When call validate_input_python "eslint-check" "fail-on-error" "1"
The status should be failure
End
End
Context "when validating numeric inputs"
It "accepts zero max-warnings"
When call validate_input_python "eslint-check" "max-warnings" "0"
The status should be success
End
It "accepts reasonable max-warnings"
When call validate_input_python "eslint-check" "max-warnings" "10"
The status should be success
End
It "accepts large max-warnings"
When call validate_input_python "eslint-check" "max-warnings" "1000"
The status should be success
End
It "accepts valid max-retries"
When call validate_input_python "eslint-check" "max-retries" "3"
The status should be success
End
It "accepts minimum retries"
When call validate_input_python "eslint-check" "max-retries" "1"
The status should be success
End
It "accepts maximum retries"
When call validate_input_python "eslint-check" "max-retries" "10"
The status should be success
End
It "rejects negative max-warnings"
When call validate_input_python "eslint-check" "max-warnings" "-1"
The status should be failure
End
It "rejects non-numeric max-warnings"
When call validate_input_python "eslint-check" "max-warnings" "many"
The status should be failure
End
It "rejects zero retries"
When call validate_input_python "eslint-check" "max-retries" "0"
The status should be failure
End
It "rejects retries above limit"
When call validate_input_python "eslint-check" "max-retries" "15"
The status should be failure
End
End
Context "when validating report-format input"
It "accepts stylish format"
When call validate_input_python "eslint-check" "report-format" "stylish"
The status should be success
End
It "accepts json format"
When call validate_input_python "eslint-check" "report-format" "json"
The status should be success
End
It "accepts sarif format"
When call validate_input_python "eslint-check" "report-format" "sarif"
The status should be success
End
It "accepts checkstyle format"
When call validate_input_python "eslint-check" "report-format" "checkstyle"
The status should be success
End
It "accepts compact format"
When call validate_input_python "eslint-check" "report-format" "compact"
The status should be success
End
It "accepts html format"
When call validate_input_python "eslint-check" "report-format" "html"
The status should be success
End
It "accepts junit format"
When call validate_input_python "eslint-check" "report-format" "junit"
The status should be success
End
It "accepts tap format"
When call validate_input_python "eslint-check" "report-format" "tap"
The status should be success
End
It "accepts unix format"
When call validate_input_python "eslint-check" "report-format" "unix"
The status should be success
End
It "rejects invalid format"
When call validate_input_python "eslint-check" "report-format" "invalid"
The status should be failure
End
It "rejects empty format"
When call validate_input_python "eslint-check" "report-format" ""
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "ESLint Check"
End
It "defines required inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "working-directory"
The output should include "eslint-version"
The output should include "max-retries"
End
It "defines optional inputs with defaults"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "config-file"
The output should include "ignore-file"
The output should include "file-extensions"
The output should include "cache"
The output should include "max-warnings"
The output should include "fail-on-error"
The output should include "report-format"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "error-count"
The output should include "warning-count"
The output should include "sarif-file"
The output should include "files-checked"
End
It "has composite run type"
When call grep -q "using: composite" "$ACTION_FILE"
The status should be success
End
It "includes input validation step"
When call grep -q "Validate Inputs" "$ACTION_FILE"
The status should be success
End
It "uses node-setup action"
When call grep -q "./node-setup" "$ACTION_FILE"
The status should be success
End
It "uses common-cache action"
When call grep -q "./common-cache" "$ACTION_FILE"
The status should be success
End
End
Context "when validating security"
It "validates input paths to prevent injection"
When call validate_input_python "eslint-check" "working-directory" "../../../etc"
The status should be failure
End
It "validates config file paths"
When call validate_input_python "eslint-check" "config-file" "../../malicious.js"
The status should be failure
End
It "sanitizes file extensions input"
When call validate_input_python "eslint-check" "file-extensions" ".js;rm -rf /"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs"
When call test_action_outputs "$ACTION_DIR" "working-directory" "." "eslint-version" "latest" "max-retries" "3"
The status should be success
The stderr should include "Testing action outputs for: eslint-check"
The stderr should include "Output test passed for: eslint-check"
End
It "outputs consistent error and warning counts"
When call test_action_outputs "$ACTION_DIR" "max-warnings" "0" "report-format" "sarif"
The status should be success
The stderr should include "Testing action outputs for: eslint-check"
The stderr should include "Output test passed for: eslint-check"
End
End
End

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env shellspec
# Unit tests for eslint-fix action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "eslint-fix action"
ACTION_DIR="eslint-fix"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts valid GitHub token"
When call validate_input_python "eslint-fix" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "rejects injection in token"
When call validate_input_python "eslint-fix" "token" "token; rm -rf /"
The status should be failure
End
End
Context "when validating username input"
It "accepts valid username"
When call validate_input_python "eslint-fix" "username" "github-actions"
The status should be success
End
It "rejects injection in username"
When call validate_input_python "eslint-fix" "username" "user; rm -rf /"
The status should be failure
End
End
Context "when validating email input"
It "accepts valid email"
When call validate_input_python "eslint-fix" "email" "test@example.com"
The status should be success
End
It "rejects invalid email format"
When call validate_input_python "eslint-fix" "email" "invalid-email"
The status should be failure
End
End
Context "when validating numeric inputs"
It "accepts valid max-retries"
When call validate_input_python "eslint-fix" "max-retries" "3"
The status should be success
End
It "accepts minimum retries"
When call validate_input_python "eslint-fix" "max-retries" "1"
The status should be success
End
It "accepts maximum retries"
When call validate_input_python "eslint-fix" "max-retries" "10"
The status should be success
End
It "rejects zero retries"
When call validate_input_python "eslint-fix" "max-retries" "0"
The status should be failure
End
It "rejects retries above limit"
When call validate_input_python "eslint-fix" "max-retries" "15"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "ESLint Fix"
End
It "defines required inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "token"
The output should include "username"
The output should include "email"
The output should include "max-retries"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "files_changed"
The output should include "lint_status"
The output should include "errors_fixed"
End
End
Context "when validating security"
It "validates token format"
When call validate_input_python "eslint-fix" "token" "invalid-token;rm -rf /"
The status should be failure
End
It "validates email format"
When call validate_input_python "eslint-fix" "email" "invalid@email"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs"
When call test_action_outputs "$ACTION_DIR" "token" "ghp_test" "username" "test" "email" "test@example.com" "max-retries" "3"
The status should be success
The stderr should include "Testing action outputs for: eslint-fix"
The stderr should include "Output test passed for: eslint-fix"
End
End
End

View File

@@ -0,0 +1,141 @@
#!/usr/bin/env shellspec
# Unit tests for github-release action validation and logic
# Framework is automatically loaded via spec_helper.sh
# Using the centralized validate_input_python function from spec_helper.sh
Describe "github-release action"
ACTION_DIR="github-release"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating version input"
It "accepts valid semantic version"
When call validate_input_python "github-release" "version" "1.2.3"
The status should be success
End
It "accepts semantic version with v prefix"
When call validate_input_python "github-release" "version" "v1.2.3"
The status should be success
End
It "accepts prerelease version"
When call validate_input_python "github-release" "version" "1.2.3-alpha"
The status should be success
End
It "accepts build metadata version"
When call validate_input_python "github-release" "version" "1.2.3+build.1"
The status should be success
End
It "accepts prerelease with build metadata"
When call validate_input_python "github-release" "version" "1.2.3-alpha.1+build.1"
The status should be success
End
It "accepts CalVer format"
When call validate_input_python "github-release" "version" "2024.3.1"
The status should be success
End
It "rejects invalid version format"
When call validate_input_python "github-release" "version" "invalid-version"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "github-release" "version" "1.2.3; rm -rf /"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "github-release" "version" ""
The status should be failure
End
End
Context "when validating changelog input"
It "accepts empty changelog"
When call validate_input_python "github-release" "changelog" ""
The status should be success
End
It "accepts normal changelog content"
When call validate_input_python "github-release" "changelog" "## What's Changed\n- Fixed bug #123\n- Added feature X"
The status should be success
End
It "accepts changelog with special characters"
When call validate_input_python "github-release" "changelog" "Version 1.2.3\n\n- Bug fixes & improvements\n- Added @mention support"
The status should be success
End
It "rejects changelog with command injection"
When call validate_input_python "github-release" "changelog" "Release notes; rm -rf /"
The status should be failure
End
It "rejects changelog with shell expansion"
When call validate_input_python "github-release" "changelog" "Release \$(whoami) notes"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "GitHub Release"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "version"
The output should include "changelog"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "release_url"
The output should include "release_id"
The output should include "upload_url"
End
End
Context "when testing input requirements"
It "requires version input"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "version"
End
It "has changelog as optional input"
# Test that changelog has a default value in action.yml
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "changelog" "optional"
The output should equal "optional"
End
End
Context "when testing security validations"
It "validates against path traversal in version"
When call validate_input_python "github-release" "version" "../1.2.3"
The status should be failure
End
It "validates against shell metacharacters in version"
When call validate_input_python "github-release" "version" "1.2.3|echo"
The status should be failure
End
It "validates against shell metacharacters in changelog"
When call validate_input_python "github-release" "changelog" "Release notes|echo test"
The status should be failure
End
End
End

View File

@@ -0,0 +1,173 @@
#!/usr/bin/env shellspec
# Unit tests for go-build action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "go-build action"
ACTION_DIR="go-build"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating go-version input"
It "accepts valid Go version"
When call validate_input_python "go-build" "go-version" "1.21.0"
The status should be success
End
It "accepts Go version with v prefix"
When call validate_input_python "go-build" "go-version" "v1.21.0"
The status should be success
End
It "accepts newer Go version"
When call validate_input_python "go-build" "go-version" "1.22.1"
The status should be success
End
It "accepts prerelease Go version"
When call validate_input_python "go-build" "go-version" "1.21.0-rc1"
The status should be success
End
It "rejects invalid Go version format"
When call validate_input_python "go-build" "go-version" "invalid-version"
The status should be failure
End
It "rejects Go version with command injection"
When call validate_input_python "go-build" "go-version" "1.21; rm -rf /"
The status should be failure
End
End
Context "when validating destination input"
It "accepts valid relative path"
When call validate_input_python "go-build" "destination" "./bin"
The status should be success
End
It "accepts nested directory path"
When call validate_input_python "go-build" "destination" "build/output"
The status should be success
End
It "accepts simple directory name"
When call validate_input_python "go-build" "destination" "dist"
The status should be success
End
It "rejects path traversal in destination"
When call validate_input_python "go-build" "destination" "../bin"
The status should be failure
End
It "rejects absolute path"
When call validate_input_python "go-build" "destination" "/usr/bin"
The status should be failure
End
It "rejects destination with command injection"
When call validate_input_python "go-build" "destination" "./bin; rm -rf /"
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts valid retry count"
When call validate_input_python "go-build" "max-retries" "3"
The status should be success
End
It "accepts minimum retry count"
When call validate_input_python "go-build" "max-retries" "1"
The status should be success
End
It "accepts maximum retry count"
When call validate_input_python "go-build" "max-retries" "10"
The status should be success
End
It "rejects retry count below minimum"
When call validate_input_python "go-build" "max-retries" "0"
The status should be failure
End
It "rejects retry count above maximum"
When call validate_input_python "go-build" "max-retries" "15"
The status should be failure
End
It "rejects non-numeric retry count"
When call validate_input_python "go-build" "max-retries" "many"
The status should be failure
End
It "rejects decimal retry count"
When call validate_input_python "go-build" "max-retries" "3.5"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Go Build"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "go-version"
The output should include "destination"
The output should include "max-retries"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "build_status"
The output should include "test_status"
The output should include "go_version"
The output should include "binary_path"
The output should include "coverage_path"
End
End
Context "when testing input defaults"
It "has default destination"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "destination" "default"
The output should equal "./bin"
End
It "has default max-retries"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "max-retries" "default"
The output should equal "3"
End
It "has all inputs as optional"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "" "all_optional"
The output should equal "none"
End
End
Context "when testing security validations"
It "validates against shell injection in go-version"
When call validate_input_python "go-build" "go-version" "1.21.0|echo test"
The status should be failure
End
It "validates against shell injection in destination"
When call validate_input_python "go-build" "destination" "bin\$(whoami)"
The status should be failure
End
It "validates against shell injection in max-retries"
When call validate_input_python "go-build" "max-retries" "3;echo test"
The status should be failure
End
End
End

View File

@@ -0,0 +1,255 @@
#!/usr/bin/env shellspec
# Unit tests for go-lint action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "go-lint action"
ACTION_DIR="go-lint"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating working-directory input"
It "accepts current directory"
When call validate_input_python "go-lint" "working-directory" "."
The status should be success
End
It "accepts relative directory path"
When call validate_input_python "go-lint" "working-directory" "src/main"
The status should be success
End
It "rejects path traversal"
When call validate_input_python "go-lint" "working-directory" "../src"
The status should be failure
End
It "rejects absolute path"
When call validate_input_python "go-lint" "working-directory" "/usr/src"
The status should be failure
End
End
Context "when validating golangci-lint-version input"
It "accepts latest version"
When call validate_input_python "go-lint" "golangci-lint-version" "latest"
The status should be success
End
It "accepts semantic version"
When call validate_input_python "go-lint" "golangci-lint-version" "1.55.2"
The status should be success
End
It "accepts semantic version with v prefix"
When call validate_input_python "go-lint" "golangci-lint-version" "v1.55.2"
The status should be success
End
It "rejects invalid version format"
When call validate_input_python "go-lint" "golangci-lint-version" "invalid-version"
The status should be failure
End
End
Context "when validating go-version input"
It "accepts stable version"
When call validate_input_python "go-lint" "go-version" "stable"
The status should be success
End
It "accepts major.minor version"
When call validate_input_python "go-lint" "go-version" "1.21"
The status should be success
End
It "accepts full semantic version"
When call validate_input_python "go-lint" "go-version" "1.21.5"
The status should be success
End
It "rejects invalid Go version"
When call validate_input_python "go-lint" "go-version" "go1.21"
The status should be failure
End
End
Context "when validating config-file input"
It "accepts default config file"
When call validate_input_python "go-lint" "config-file" ".golangci.yml"
The status should be success
End
It "accepts custom config file path"
When call validate_input_python "go-lint" "config-file" "configs/golangci.yaml"
The status should be success
End
It "rejects path traversal in config file"
When call validate_input_python "go-lint" "config-file" "../configs/golangci.yml"
The status should be failure
End
End
Context "when validating timeout input"
It "accepts timeout in minutes"
When call validate_input_python "go-lint" "timeout" "5m"
The status should be success
End
It "accepts timeout in seconds"
When call validate_input_python "go-lint" "timeout" "300s"
The status should be success
End
It "accepts timeout in hours"
When call validate_input_python "go-lint" "timeout" "1h"
The status should be success
End
It "rejects timeout without unit"
When call validate_input_python "go-lint" "timeout" "300"
The status should be failure
End
It "rejects invalid timeout format"
When call validate_input_python "go-lint" "timeout" "5 minutes"
The status should be failure
End
End
Context "when validating boolean inputs"
It "accepts true for cache"
When call validate_input_python "go-lint" "cache" "true"
The status should be success
End
It "accepts false for cache"
When call validate_input_python "go-lint" "cache" "false"
The status should be success
End
It "rejects invalid boolean for fail-on-error"
When call validate_input_python "go-lint" "fail-on-error" "maybe"
The status should be failure
End
It "accepts true for only-new-issues"
When call validate_input_python "go-lint" "only-new-issues" "true"
The status should be success
End
It "accepts false for disable-all"
When call validate_input_python "go-lint" "disable-all" "false"
The status should be success
End
End
Context "when validating report-format input"
It "accepts sarif format"
When call validate_input_python "go-lint" "report-format" "sarif"
The status should be success
End
It "accepts json format"
When call validate_input_python "go-lint" "report-format" "json"
The status should be success
End
It "accepts github-actions format"
When call validate_input_python "go-lint" "report-format" "github-actions"
The status should be success
End
It "rejects invalid report format"
When call validate_input_python "go-lint" "report-format" "invalid-format"
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts valid retry count"
When call validate_input_python "go-lint" "max-retries" "3"
The status should be success
End
It "accepts minimum retry count"
When call validate_input_python "go-lint" "max-retries" "1"
The status should be success
End
It "accepts maximum retry count"
When call validate_input_python "go-lint" "max-retries" "10"
The status should be success
End
It "rejects retry count below minimum"
When call validate_input_python "go-lint" "max-retries" "0"
The status should be failure
End
It "rejects retry count above maximum"
When call validate_input_python "go-lint" "max-retries" "15"
The status should be failure
End
End
Context "when validating linter lists"
It "accepts valid enable-linters list"
When call validate_input_python "go-lint" "enable-linters" "gosec,govet,staticcheck"
The status should be success
End
It "accepts single linter in enable-linters"
When call validate_input_python "go-lint" "enable-linters" "gosec"
The status should be success
End
It "accepts valid disable-linters list"
When call validate_input_python "go-lint" "disable-linters" "exhaustivestruct,interfacer"
The status should be success
End
It "rejects invalid linter list format"
When call validate_input_python "go-lint" "enable-linters" "gosec, govet"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Go Lint Check"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "error-count"
The output should include "sarif-file"
The output should include "cache-hit"
The output should include "analyzed-files"
End
End
Context "when testing security validations"
It "validates against command injection in working-directory"
When call validate_input_python "go-lint" "working-directory" "src; rm -rf /"
The status should be failure
End
It "validates against command injection in config-file"
When call validate_input_python "go-lint" "config-file" "config.yml\$(whoami)"
The status should be failure
End
It "validates against shell expansion in enable-linters"
When call validate_input_python "go-lint" "enable-linters" "gosec,\$(echo malicious)"
The status should be failure
End
End
End

View File

@@ -0,0 +1,171 @@
#!/usr/bin/env shellspec
# Unit tests for go-version-detect action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "go-version-detect action"
ACTION_DIR="go-version-detect"
ACTION_FILE="$ACTION_DIR/action.yml"
# Test version constants (update these when Go releases new versions)
CURRENT_STABLE_GO_VERSION="1.25"
CURRENT_STABLE_GO_PATCH="1.25.0"
PREVIOUS_GO_VERSION="1.24.0"
MIN_SUPPORTED_GO_VERSION="1.18"
MAX_SUPPORTED_GO_VERSION="1.30"
TOO_OLD_GO_VERSION="1.17"
TOO_NEW_GO_VERSION="1.31"
Context "when validating default-version input"
It "accepts valid semantic version"
When call validate_input_python "go-version-detect" "default-version" "$CURRENT_STABLE_GO_VERSION"
The status should be success
End
It "accepts semantic version with patch"
When call validate_input_python "go-version-detect" "default-version" "$PREVIOUS_GO_VERSION"
The status should be success
End
It "accepts minimum supported Go version"
When call validate_input_python "go-version-detect" "default-version" "$MIN_SUPPORTED_GO_VERSION"
The status should be success
End
It "accepts current stable Go version"
When call validate_input_python "go-version-detect" "default-version" "$CURRENT_STABLE_GO_PATCH"
The status should be success
End
It "rejects version without minor"
When call validate_input_python "go-version-detect" "default-version" "1"
The status should be failure
End
It "rejects invalid version format"
When call validate_input_python "go-version-detect" "default-version" "invalid-version"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}; rm -rf /"
The status should be failure
End
It "rejects version with shell expansion"
When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}\$(echo test)"
The status should be failure
End
It "rejects major version other than 1"
When call validate_input_python "go-version-detect" "default-version" "2.0"
The status should be failure
End
It "rejects too old minor version"
When call validate_input_python "go-version-detect" "default-version" "$TOO_OLD_GO_VERSION"
The status should be failure
End
It "rejects too new minor version"
When call validate_input_python "go-version-detect" "default-version" "$TOO_NEW_GO_VERSION"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "go-version-detect" "default-version" ""
The status should be failure
End
It "rejects version with leading v"
When call validate_input_python "go-version-detect" "default-version" "v${CURRENT_STABLE_GO_VERSION}"
The status should be failure
End
It "rejects version with prerelease"
When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}-beta"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Go Version Detect"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "default-version"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "go-version"
End
End
Context "when testing input requirements"
It "has default-version as optional input"
# Test that default-version has a default value in action.yml
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "optional"
The output should equal "optional"
End
It "has correct default version"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "default"
The output should equal "$CURRENT_STABLE_GO_VERSION"
End
End
Context "when testing security validations"
It "validates against path traversal in version"
When call validate_input_python "go-version-detect" "default-version" "../${CURRENT_STABLE_GO_VERSION}"
The status should be failure
End
It "validates against shell metacharacters in version"
When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}|echo"
The status should be failure
End
It "validates against backtick injection"
When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}\`whoami\`"
The status should be failure
End
It "validates against variable expansion"
When call validate_input_python "go-version-detect" "default-version" "${CURRENT_STABLE_GO_VERSION}\${HOME}"
The status should be failure
End
End
Context "when testing version range validation"
It "validates reasonable Go version range boundaries"
# Test boundary conditions for Go version validation
When call validate_input_python "go-version-detect" "default-version" "$TOO_OLD_GO_VERSION"
The status should be failure
End
It "validates upper boundary"
When call validate_input_python "go-version-detect" "default-version" "$TOO_NEW_GO_VERSION"
The status should be failure
End
It "validates exact boundary valid values"
When call validate_input_python "go-version-detect" "default-version" "$MIN_SUPPORTED_GO_VERSION"
The status should be success
End
It "validates exact boundary valid values upper"
When call validate_input_python "go-version-detect" "default-version" "$MAX_SUPPORTED_GO_VERSION"
The status should be success
End
End
End

View File

@@ -0,0 +1,242 @@
#!/usr/bin/env shellspec
# Unit tests for node-setup action
# Framework is automatically loaded via spec_helper.sh
Describe "node-setup action"
ACTION_DIR="node-setup"
ACTION_FILE="$ACTION_DIR/action.yml"
# Framework is automatically initialized via spec_helper.sh
Context "when validating inputs"
It "accepts valid Node.js version"
When call validate_input_python "node-setup" "default-version" "18.17.0"
The status should be success
End
It "accepts valid package manager"
When call validate_input_python "node-setup" "package-manager" "npm"
The status should be success
End
It "accepts yarn as package manager"
When call validate_input_python "node-setup" "package-manager" "yarn"
The status should be success
End
It "accepts pnpm as package manager"
When call validate_input_python "node-setup" "package-manager" "pnpm"
The status should be success
End
It "accepts bun as package manager"
When call validate_input_python "node-setup" "package-manager" "bun"
The status should be success
End
It "rejects invalid package manager"
When call validate_input_python "node-setup" "package-manager" "invalid-manager"
The status should be failure
End
It "rejects malformed Node.js version"
When call validate_input_python "node-setup" "default-version" "not-a-version"
The status should be failure
End
It "rejects command injection in inputs"
When call validate_input_python "node-setup" "default-version" "18.0.0; rm -rf /"
The status should be failure
End
End
Context "when checking action structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
When call get_action_name "$ACTION_FILE"
The output should equal "Node Setup"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "default-version"
The output should include "package-manager"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "node-version"
The output should include "package-manager"
The output should include "cache-hit"
End
End
Context "when testing Node.js version detection"
BeforeEach "shellspec_setup_test_env 'node-version-detection'"
AfterEach "shellspec_cleanup_test_env 'node-version-detection'"
It "detects version from package.json engines field"
create_mock_node_repo
# Mock action output based on package.json
echo "node-version=18.0.0" >>"$GITHUB_OUTPUT"
When call shellspec_validate_action_output "node-version" "18.0.0"
The status should be success
End
It "detects version from .nvmrc file"
create_mock_node_repo
echo "18.17.1" >.nvmrc
# Mock action output
echo "node-version=18.17.1" >>"$GITHUB_OUTPUT"
When call shellspec_validate_action_output "node-version" "18.17.1"
The status should be success
End
It "uses default version when none specified"
create_mock_node_repo
# Remove engines field simulation
# Mock default version output
echo "node-version=20.0.0" >>"$GITHUB_OUTPUT"
When call shellspec_validate_action_output "node-version" "20.0.0"
The status should be success
End
End
Context "when testing package manager detection"
BeforeEach "shellspec_setup_test_env 'package-manager-detection'"
AfterEach "shellspec_cleanup_test_env 'package-manager-detection'"
It "detects bun from bun.lockb"
create_mock_node_repo
touch bun.lockb
echo "package-manager=bun" >>"$GITHUB_OUTPUT"
When call shellspec_validate_action_output "package-manager" "bun"
The status should be success
End
It "detects pnpm from pnpm-lock.yaml"
create_mock_node_repo
touch pnpm-lock.yaml
echo "package-manager=pnpm" >>"$GITHUB_OUTPUT"
When call shellspec_validate_action_output "package-manager" "pnpm"
The status should be success
End
It "detects yarn from yarn.lock"
create_mock_node_repo
touch yarn.lock
echo "package-manager=yarn" >>"$GITHUB_OUTPUT"
When call shellspec_validate_action_output "package-manager" "yarn"
The status should be success
End
It "detects npm from package-lock.json"
create_mock_node_repo
touch package-lock.json
echo "package-manager=npm" >>"$GITHUB_OUTPUT"
When call shellspec_validate_action_output "package-manager" "npm"
The status should be success
End
It "detects packageManager field from package.json"
create_mock_node_repo
# Add packageManager field to package.json
cat >package.json <<EOF
{
"name": "test-project",
"version": "1.0.0",
"packageManager": "pnpm@8.0.0",
"engines": {
"node": ">=18.0.0"
}
}
EOF
echo "package-manager=pnpm" >>"$GITHUB_OUTPUT"
When call shellspec_validate_action_output "package-manager" "pnpm"
The status should be success
End
End
Context "when testing Corepack integration"
BeforeEach "shellspec_setup_test_env 'corepack-test'"
AfterEach "shellspec_cleanup_test_env 'corepack-test'"
It "enables Corepack when packageManager is specified"
create_mock_node_repo
# Simulate packageManager field
cat >package.json <<EOF
{
"name": "test-project",
"version": "1.0.0",
"packageManager": "yarn@3.6.0"
}
EOF
# Mock Corepack enabled output
echo "corepack-enabled=true" >>"$GITHUB_OUTPUT"
When call shellspec_validate_action_output "corepack-enabled" "true"
The status should be success
End
End
Context "when testing cache functionality"
BeforeEach "shellspec_setup_test_env 'cache-test'"
AfterEach "shellspec_cleanup_test_env 'cache-test'"
It "reports cache hit when dependencies are cached"
create_mock_node_repo
touch package-lock.json
mkdir -p node_modules
# Mock cache hit
echo "cache-hit=true" >>"$GITHUB_OUTPUT"
When call shellspec_validate_action_output "cache-hit" "true"
The status should be success
End
It "reports cache miss when no cache exists"
create_mock_node_repo
touch package-lock.json
# Mock cache miss
echo "cache-hit=false" >>"$GITHUB_OUTPUT"
When call shellspec_validate_action_output "cache-hit" "false"
The status should be success
End
End
Context "when testing output consistency"
It "produces all expected outputs"
When call test_action_outputs "$ACTION_DIR" "node-version" "18.0.0" "package-manager" "npm"
The status should be success
The stderr should include "Testing action outputs for: node-setup"
The stderr should include "Output test passed for: node-setup"
End
End
End

View File

@@ -0,0 +1,216 @@
#!/usr/bin/env shellspec
# Unit tests for npm-publish action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "npm-publish action"
ACTION_DIR="npm-publish"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating registry-url input"
It "accepts valid https registry URL"
When call validate_input_python "npm-publish" "registry-url" "https://registry.npmjs.org/"
The status should be success
End
It "accepts https registry URL without trailing slash"
When call validate_input_python "npm-publish" "registry-url" "https://registry.npmjs.org"
The status should be success
End
It "accepts http registry URL"
When call validate_input_python "npm-publish" "registry-url" "http://localhost:4873"
The status should be success
End
It "accepts registry URL with path"
When call validate_input_python "npm-publish" "registry-url" "https://npm.example.com/registry/"
The status should be success
End
It "rejects non-http(s) URL"
When call validate_input_python "npm-publish" "registry-url" "ftp://registry.example.com"
The status should be failure
End
It "rejects invalid URL format"
When call validate_input_python "npm-publish" "registry-url" "not-a-url"
The status should be failure
End
End
Context "when validating npm_token input"
It "accepts valid GitHub token format (exact length)"
When call validate_input_python "npm-publish" "npm_token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "accepts valid NPM classic token format"
When call validate_input_python "npm-publish" "npm_token" "npm_1234567890123456789012345678901234567890"
The status should be success
End
It "accepts GitHub fine-grained token (exact length)"
When call validate_input_python "npm-publish" "npm_token" "github_pat_1234567890123456789012345678901234567890123456789012345678901234567890a"
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "npm-publish" "npm_token" "invalid-token-format"
The status should be failure
End
It "rejects empty token"
When call validate_input_python "npm-publish" "npm_token" ""
The status should be failure
End
It "rejects token with command injection"
When call validate_input_python "npm-publish" "npm_token" "ghp_123456789012345678901234567890123456; rm -rf /"
The status should be failure
End
End
Context "when validating scope input"
It "accepts valid npm scope"
When call validate_input_python "npm-publish" "scope" "@myorg"
The status should be success
End
It "accepts scope with hyphens"
When call validate_input_python "npm-publish" "scope" "@my-organization"
The status should be success
End
It "accepts scope with numbers"
When call validate_input_python "npm-publish" "scope" "@myorg123"
The status should be success
End
It "rejects scope without @ prefix"
When call validate_input_python "npm-publish" "scope" "myorg"
The status should be failure
End
It "rejects scope with invalid characters"
When call validate_input_python "npm-publish" "scope" "@my_org!"
The status should be failure
End
It "rejects scope with command injection"
When call validate_input_python "npm-publish" "scope" "@myorg; rm -rf /"
The status should be failure
End
End
Context "when validating access input"
It "accepts public access"
When call validate_input_python "npm-publish" "access" "public"
The status should be success
End
It "accepts restricted access"
When call validate_input_python "npm-publish" "access" "restricted"
The status should be success
End
It "accepts private access (no specific validation)"
When call validate_input_python "npm-publish" "access" "private"
The status should be success
End
It "accepts empty access"
When call validate_input_python "npm-publish" "access" ""
The status should be success
End
End
Context "when validating provenance input"
It "accepts true for provenance"
When call validate_input_python "npm-publish" "provenance" "true"
The status should be success
End
It "accepts false for provenance"
When call validate_input_python "npm-publish" "provenance" "false"
The status should be success
End
It "accepts any value for provenance (no specific validation)"
When call validate_input_python "npm-publish" "provenance" "maybe"
The status should be success
End
End
Context "when validating dry-run input"
It "accepts true for dry-run"
When call validate_input_python "npm-publish" "dry-run" "true"
The status should be success
End
It "accepts false for dry-run"
When call validate_input_python "npm-publish" "dry-run" "false"
The status should be success
End
It "accepts any value for dry-run (no specific validation)"
When call validate_input_python "npm-publish" "dry-run" "yes"
The status should be success
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Publish to NPM"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "npm_token"
The output should include "registry-url"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "registry-url"
The output should include "scope"
The output should include "package-version"
End
End
Context "when testing input requirements"
It "requires npm_token input"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "npm_token"
End
It "has registry-url as optional with default"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "registry-url" "default"
The output should include "registry.npmjs.org"
End
End
Context "when testing security validations"
It "validates against path traversal in all inputs"
When call validate_input_python "npm-publish" "scope" "@../../../etc"
The status should be failure
End
It "validates against shell metacharacters"
When call validate_input_python "npm-publish" "registry-url" "https://registry.npmjs.org|echo"
The status should be failure
End
It "validates against command substitution"
When call validate_input_python "npm-publish" "scope" "@\$(whoami)"
The status should be failure
End
End
End

View File

@@ -0,0 +1,407 @@
#!/usr/bin/env shellspec
# Unit tests for php-composer action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "php-composer action"
ACTION_DIR="php-composer"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating php input"
It "accepts valid PHP version"
When call validate_input_python "php-composer" "php" "8.4"
The status should be success
End
It "accepts PHP version with patch"
When call validate_input_python "php-composer" "php" "8.4.1"
The status should be success
End
It "accepts PHP 7.4"
When call validate_input_python "php-composer" "php" "7.4"
The status should be success
End
It "accepts PHP 8.0"
When call validate_input_python "php-composer" "php" "8.0"
The status should be success
End
It "accepts PHP 8.1"
When call validate_input_python "php-composer" "php" "8.1"
The status should be success
End
It "rejects PHP version too old"
When call validate_input_python "php-composer" "php" "5.5"
The status should be failure
End
It "rejects invalid version format"
When call validate_input_python "php-composer" "php" "php8.4"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "php-composer" "php" "8.4; rm -rf /"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "php-composer" "php" ""
The status should be failure
End
End
Context "when validating extensions input"
It "accepts valid PHP extensions"
When call validate_input_python "php-composer" "extensions" "mbstring, xml, zip"
The status should be success
End
It "accepts single extension"
When call validate_input_python "php-composer" "extensions" "mbstring"
The status should be success
End
It "accepts extensions without spaces"
When call validate_input_python "php-composer" "extensions" "mbstring,xml,zip"
The status should be success
End
It "accepts extensions with underscores"
When call validate_input_python "php-composer" "extensions" "pdo_mysql, gd_jpeg"
The status should be success
End
It "rejects extensions with special characters"
When call validate_input_python "php-composer" "extensions" "mbstring@xml"
The status should be failure
End
It "rejects extensions with command injection"
When call validate_input_python "php-composer" "extensions" "mbstring; rm -rf /"
The status should be failure
End
It "rejects empty extensions"
When call validate_input_python "php-composer" "extensions" ""
The status should be failure
End
End
Context "when validating tools input"
It "accepts valid Composer tools"
When call validate_input_python "php-composer" "tools" "composer:v2"
The status should be success
End
It "accepts multiple tools"
When call validate_input_python "php-composer" "tools" "composer:v2, phpunit:^9.0"
The status should be success
End
It "accepts tools with version constraints"
When call validate_input_python "php-composer" "tools" "phpcs, phpstan:1.10"
The status should be success
End
It "accepts tools with stability flags (@ allowed)"
When call validate_input_python "php-composer" "tools" "dev-master@dev"
The status should be success
End
It "accepts tools with version and stability flag"
When call validate_input_python "php-composer" "tools" "monolog/monolog@dev"
The status should be success
End
It "rejects tools with backticks"
When call validate_input_python "php-composer" "tools" "composer\`whoami\`"
The status should be failure
End
It "rejects tools with command injection"
When call validate_input_python "php-composer" "tools" "composer; rm -rf /"
The status should be failure
End
It "rejects empty tools"
When call validate_input_python "php-composer" "tools" ""
The status should be failure
End
End
Context "when validating composer-version input"
It "accepts composer version 1"
When call validate_input_python "php-composer" "composer-version" "1"
The status should be success
End
It "accepts composer version 2"
When call validate_input_python "php-composer" "composer-version" "2"
The status should be success
End
It "rejects invalid composer version"
When call validate_input_python "php-composer" "composer-version" "3"
The status should be failure
End
It "rejects non-numeric composer version"
When call validate_input_python "php-composer" "composer-version" "latest"
The status should be failure
End
It "rejects empty composer version"
When call validate_input_python "php-composer" "composer-version" ""
The status should be failure
End
End
Context "when validating stability input"
It "accepts stable"
When call validate_input_python "php-composer" "stability" "stable"
The status should be success
End
It "accepts RC"
When call validate_input_python "php-composer" "stability" "RC"
The status should be success
End
It "accepts beta"
When call validate_input_python "php-composer" "stability" "beta"
The status should be success
End
It "accepts alpha"
When call validate_input_python "php-composer" "stability" "alpha"
The status should be success
End
It "accepts dev"
When call validate_input_python "php-composer" "stability" "dev"
The status should be success
End
It "rejects invalid stability"
When call validate_input_python "php-composer" "stability" "unstable"
The status should be failure
End
It "rejects stability with injection"
When call validate_input_python "php-composer" "stability" "stable; rm -rf /"
The status should be failure
End
End
Context "when validating cache-directories input"
It "accepts valid cache directory"
When call validate_input_python "php-composer" "cache-directories" "vendor/cache"
The status should be success
End
It "accepts multiple cache directories"
When call validate_input_python "php-composer" "cache-directories" "vendor/cache, .cache"
The status should be success
End
It "accepts directories with underscores and hyphens"
When call validate_input_python "php-composer" "cache-directories" "cache_dir, cache-dir"
The status should be success
End
It "rejects path traversal"
When call validate_input_python "php-composer" "cache-directories" "../malicious"
The status should be failure
End
It "rejects absolute paths"
When call validate_input_python "php-composer" "cache-directories" "/etc/passwd"
The status should be failure
End
It "rejects directories with command injection"
When call validate_input_python "php-composer" "cache-directories" "cache; rm -rf /"
The status should be failure
End
It "rejects empty cache directories"
When call validate_input_python "php-composer" "cache-directories" ""
The status should be failure
End
End
Context "when validating token input"
It "accepts GitHub token expression"
When call validate_input_python "php-composer" "token" "\${{ github.token }}"
The status should be success
End
It "accepts GitHub fine-grained token"
When call validate_input_python "php-composer" "token" "ghp_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "accepts GitHub app token"
When call validate_input_python "php-composer" "token" "ghs_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "php-composer" "token" "invalid-token"
The status should be failure
End
It "rejects empty token"
When call validate_input_python "php-composer" "token" ""
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts valid retry count"
When call validate_input_python "php-composer" "max-retries" "3"
The status should be success
End
It "accepts minimum retries"
When call validate_input_python "php-composer" "max-retries" "1"
The status should be success
End
It "accepts maximum retries"
When call validate_input_python "php-composer" "max-retries" "10"
The status should be success
End
It "rejects zero retries"
When call validate_input_python "php-composer" "max-retries" "0"
The status should be failure
End
It "rejects too many retries"
When call validate_input_python "php-composer" "max-retries" "11"
The status should be failure
End
It "rejects non-numeric retries"
When call validate_input_python "php-composer" "max-retries" "many"
The status should be failure
End
It "rejects negative retries"
When call validate_input_python "php-composer" "max-retries" "-1"
The status should be failure
End
End
Context "when validating args input"
It "accepts valid Composer arguments"
When call validate_input_python "php-composer" "args" "--no-progress --prefer-dist"
The status should be success
End
It "rejects empty args"
When call validate_input_python "php-composer" "args" ""
The status should be failure
End
It "rejects args with command injection"
When call validate_input_python "php-composer" "args" "--no-progress; rm -rf /"
The status should be failure
End
It "rejects args with pipe"
When call validate_input_python "php-composer" "args" "--no-progress | cat /etc/passwd"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Run Composer Install"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "php"
The output should include "extensions"
The output should include "tools"
The output should include "args"
The output should include "composer-version"
The output should include "stability"
The output should include "cache-directories"
The output should include "token"
The output should include "max-retries"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "lock"
The output should include "php-version"
The output should include "composer-version"
The output should include "cache-hit"
End
End
Context "when testing input requirements"
It "requires php input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "php" "required"
The output should equal "required"
End
It "has extensions as optional input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "extensions" "optional"
The output should equal "optional"
End
End
Context "when testing security validations"
It "validates against path traversal in cache directories"
When call validate_input_python "php-composer" "cache-directories" "../../etc/passwd"
The status should be failure
End
It "validates against shell metacharacters in tools"
When call validate_input_python "php-composer" "tools" "composer && rm -rf /"
The status should be failure
End
It "validates against backtick injection in args"
When call validate_input_python "php-composer" "args" "--no-progress \`whoami\`"
The status should be failure
End
It "validates against variable expansion in extensions"
When call validate_input_python "php-composer" "extensions" "mbstring,\${HOME}"
The status should be failure
End
End
Context "when testing PHP-specific validations"
It "validates PHP version boundaries"
When call validate_input_python "php-composer" "php" "10.0"
The status should be failure
End
It "validates Composer version enum restriction"
When call validate_input_python "php-composer" "composer-version" "0"
The status should be failure
End
It "validates stability enum values"
When call validate_input_python "php-composer" "stability" "experimental"
The status should be failure
End
End
End

View File

@@ -0,0 +1,280 @@
#!/usr/bin/env shellspec
# Unit tests for php-laravel-phpunit action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "php-laravel-phpunit action"
ACTION_DIR="php-laravel-phpunit"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating php-version input"
It "accepts latest"
When call validate_input_python "php-laravel-phpunit" "php-version" "latest"
The status should be success
End
It "accepts valid PHP version"
When call validate_input_python "php-laravel-phpunit" "php-version" "8.4"
The status should be success
End
It "accepts PHP version with patch"
When call validate_input_python "php-laravel-phpunit" "php-version" "8.4.1"
The status should be success
End
It "accepts PHP 7.4"
When call validate_input_python "php-laravel-phpunit" "php-version" "7.4"
The status should be success
End
It "accepts PHP 8.0"
When call validate_input_python "php-laravel-phpunit" "php-version" "8.0"
The status should be success
End
It "rejects invalid version format"
When call validate_input_python "php-laravel-phpunit" "php-version" "php8.4"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "php-laravel-phpunit" "php-version" "8.4; rm -rf /"
The status should be failure
End
It "accepts empty version (uses default)"
When call validate_input_python "php-laravel-phpunit" "php-version" ""
The status should be success
End
End
Context "when validating php-version-file input"
It "accepts valid PHP version file"
When call validate_input_python "php-laravel-phpunit" "php-version-file" ".php-version"
The status should be success
End
It "accepts custom version file"
When call validate_input_python "php-laravel-phpunit" "php-version-file" "custom-php-version"
The status should be success
End
It "accepts version file with path"
When call validate_input_python "php-laravel-phpunit" "php-version-file" "config/.php-version"
The status should be success
End
It "rejects path traversal in version file"
When call validate_input_python "php-laravel-phpunit" "php-version-file" "../../../etc/passwd"
The status should be failure
End
It "rejects absolute path in version file"
When call validate_input_python "php-laravel-phpunit" "php-version-file" "/etc/passwd"
The status should be failure
End
It "rejects version file with command injection"
When call validate_input_python "php-laravel-phpunit" "php-version-file" ".php-version; rm -rf /"
The status should be failure
End
It "accepts empty version file"
When call validate_input_python "php-laravel-phpunit" "php-version-file" ""
The status should be success
End
End
Context "when validating extensions input"
It "accepts valid PHP extensions"
When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring, intl, json"
The status should be success
End
It "accepts single extension"
When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring"
The status should be success
End
It "accepts extensions without spaces"
When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring,intl,json"
The status should be success
End
It "accepts extensions with underscores"
When call validate_input_python "php-laravel-phpunit" "extensions" "pdo_sqlite, pdo_mysql"
The status should be success
End
It "accepts extensions with numbers"
When call validate_input_python "php-laravel-phpunit" "extensions" "sqlite3, gd2"
The status should be success
End
It "rejects extensions with special characters"
When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring@intl"
The status should be failure
End
It "rejects extensions with command injection"
When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring; rm -rf /"
The status should be failure
End
It "accepts empty extensions"
When call validate_input_python "php-laravel-phpunit" "extensions" ""
The status should be success
End
End
Context "when validating coverage input"
It "accepts none coverage"
When call validate_input_python "php-laravel-phpunit" "coverage" "none"
The status should be success
End
It "accepts xdebug coverage"
When call validate_input_python "php-laravel-phpunit" "coverage" "xdebug"
The status should be success
End
It "accepts pcov coverage"
When call validate_input_python "php-laravel-phpunit" "coverage" "pcov"
The status should be success
End
It "accepts xdebug3 coverage"
When call validate_input_python "php-laravel-phpunit" "coverage" "xdebug3"
The status should be success
End
It "rejects invalid coverage driver"
When call validate_input_python "php-laravel-phpunit" "coverage" "invalid"
The status should be failure
End
It "rejects coverage with command injection"
When call validate_input_python "php-laravel-phpunit" "coverage" "none; rm -rf /"
The status should be failure
End
It "accepts empty coverage"
When call validate_input_python "php-laravel-phpunit" "coverage" ""
The status should be success
End
End
Context "when validating token input"
It "accepts GitHub token expression"
When call validate_input_python "php-laravel-phpunit" "token" "\${{ github.token }}"
The status should be success
End
It "accepts GitHub fine-grained token"
When call validate_input_python "php-laravel-phpunit" "token" "ghp_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "accepts GitHub app token"
When call validate_input_python "php-laravel-phpunit" "token" "ghs_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "php-laravel-phpunit" "token" "invalid-token"
The status should be failure
End
It "accepts empty token"
When call validate_input_python "php-laravel-phpunit" "token" ""
The status should be success
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Laravel Setup and Composer test"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "php-version"
The output should include "php-version-file"
The output should include "extensions"
The output should include "coverage"
The output should include "token"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "php-version"
The output should include "php-version-file"
The output should include "extensions"
The output should include "coverage"
End
End
Context "when testing input requirements"
It "has all inputs as optional"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "" "all_optional"
The output should equal "none"
End
It "has correct default php-version"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "php-version" "default"
The output should equal "latest"
End
It "has correct default php-version-file"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "php-version-file" "default"
The output should equal ".php-version"
End
End
Context "when testing security validations"
It "validates against path traversal in php-version-file"
When call validate_input_python "php-laravel-phpunit" "php-version-file" "../../etc/passwd"
The status should be failure
End
It "validates against shell metacharacters in extensions"
When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring && rm -rf /"
The status should be failure
End
It "validates against backtick injection in coverage"
When call validate_input_python "php-laravel-phpunit" "coverage" "none\`whoami\`"
The status should be failure
End
It "validates against variable expansion in php-version"
When call validate_input_python "php-laravel-phpunit" "php-version" "8.4\${HOME}"
The status should be failure
End
End
Context "when testing Laravel-specific validations"
It "validates coverage driver enum values"
When call validate_input_python "php-laravel-phpunit" "coverage" "invalid-driver"
The status should be failure
End
It "validates php-version-file path safety"
When call validate_input_python "php-laravel-phpunit" "php-version-file" "/etc/shadow"
The status should be failure
End
It "validates extensions format for Laravel requirements"
When call validate_input_python "php-laravel-phpunit" "extensions" "mbstring, intl, json, pdo_sqlite, sqlite3"
The status should be success
End
End
End

View File

@@ -0,0 +1,249 @@
#!/usr/bin/env shellspec
# Unit tests for php-tests action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "php-tests action"
ACTION_DIR="php-tests"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts GitHub token expression"
When call validate_input_python "php-tests" "token" "\${{ github.token }}"
The status should be success
End
It "accepts GitHub fine-grained token"
When call validate_input_python "php-tests" "token" "ghp_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "accepts GitHub app token"
When call validate_input_python "php-tests" "token" "ghs_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "accepts GitHub enterprise token"
When call validate_input_python "php-tests" "token" "ghe_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "php-tests" "token" "invalid-token"
The status should be failure
End
It "rejects token with command injection"
When call validate_input_python "php-tests" "token" "ghp_token; rm -rf /"
The status should be failure
End
It "accepts empty token (uses default)"
When call validate_input_python "php-tests" "token" ""
The status should be success
End
End
Context "when validating username input"
It "accepts valid GitHub username"
When call validate_input_python "php-tests" "username" "github-actions"
The status should be success
End
It "accepts username with hyphens"
When call validate_input_python "php-tests" "username" "user-name"
The status should be success
End
It "accepts username with numbers"
When call validate_input_python "php-tests" "username" "user123"
The status should be success
End
It "accepts single character username"
When call validate_input_python "php-tests" "username" "a"
The status should be success
End
It "accepts maximum length username"
When call validate_input_python "php-tests" "username" "abcdefghijklmnopqrstuvwxyz0123456789abc"
The status should be success
End
It "rejects username too long"
When call validate_input_python "php-tests" "username" "abcdefghijklmnopqrstuvwxyz0123456789abcd"
The status should be failure
End
It "rejects username with command injection semicolon"
When call validate_input_python "php-tests" "username" "user; rm -rf /"
The status should be failure
End
It "rejects username with command injection ampersand"
When call validate_input_python "php-tests" "username" "user && rm -rf /"
The status should be failure
End
It "rejects username with command injection pipe"
When call validate_input_python "php-tests" "username" "user | rm -rf /"
The status should be failure
End
It "accepts empty username (uses default)"
When call validate_input_python "php-tests" "username" ""
The status should be success
End
End
Context "when validating email input"
It "accepts valid email"
When call validate_input_python "php-tests" "email" "user@example.com"
The status should be success
End
It "accepts email with subdomain"
When call validate_input_python "php-tests" "email" "user@mail.example.com"
The status should be success
End
It "accepts email with plus sign"
When call validate_input_python "php-tests" "email" "user+tag@example.com"
The status should be success
End
It "accepts email with numbers"
When call validate_input_python "php-tests" "email" "user123@example123.com"
The status should be success
End
It "accepts email with hyphens"
When call validate_input_python "php-tests" "email" "user-name@example-domain.com"
The status should be success
End
It "rejects email without at symbol"
When call validate_input_python "php-tests" "email" "userexample.com"
The status should be failure
End
It "rejects email without domain"
When call validate_input_python "php-tests" "email" "user@"
The status should be failure
End
It "rejects email without username"
When call validate_input_python "php-tests" "email" "@example.com"
The status should be failure
End
It "rejects email without dot in domain"
When call validate_input_python "php-tests" "email" "user@example"
The status should be failure
End
It "rejects email with spaces"
When call validate_input_python "php-tests" "email" "user @example.com"
The status should be failure
End
It "accepts empty email (uses default)"
When call validate_input_python "php-tests" "email" ""
The status should be success
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "PHP Tests"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "token"
The output should include "username"
The output should include "email"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "test_status"
The output should include "tests_run"
The output should include "tests_passed"
The output should include "coverage_path"
End
End
Context "when testing input requirements"
It "has all inputs as optional"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "" "all_optional"
The output should equal "none"
End
It "has empty default token (runtime fallback)"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "token" "default"
The output should equal "no-default"
End
It "has correct default username"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "username" "default"
The output should equal "github-actions"
End
It "has correct default email"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "email" "default"
The output should equal "github-actions@github.com"
End
End
Context "when testing security validations"
It "validates against command injection in username"
When call validate_input_python "php-tests" "username" "user\`whoami\`"
The status should be failure
End
It "validates against shell metacharacters in email"
When call validate_input_python "php-tests" "email" "user@example.com; rm -rf /"
The status should be failure
End
It "validates against variable expansion in token"
When call validate_input_python "php-tests" "token" "\${MALICIOUS_VAR}"
The status should be failure
End
It "validates against backtick injection in username"
When call validate_input_python "php-tests" "username" "user\`echo malicious\`"
The status should be failure
End
End
Context "when testing PHP-specific validations"
It "validates username length boundaries"
When call validate_input_python "php-tests" "username" "$(awk 'BEGIN{for(i=1;i<=40;i++)printf "a"}')"
The status should be failure
End
It "validates email format for Git commits"
When call validate_input_python "php-tests" "email" "noreply@github.com"
The status should be success
End
It "validates default values are secure"
When call validate_input_python "php-tests" "username" "github-actions"
The status should be success
End
It "validates default email is secure"
When call validate_input_python "php-tests" "email" "github-actions@github.com"
The status should be success
End
End
End

View File

@@ -0,0 +1,170 @@
#!/usr/bin/env shellspec
# Unit tests for php-version-detect action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "php-version-detect action"
ACTION_DIR="php-version-detect"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating default-version input"
It "accepts valid PHP version"
When call validate_input_python "php-version-detect" "default-version" "8.2"
The status should be success
End
It "accepts PHP version with patch"
When call validate_input_python "php-version-detect" "default-version" "8.3.1"
The status should be success
End
It "accepts PHP 7.4"
When call validate_input_python "php-version-detect" "default-version" "7.4"
The status should be success
End
It "accepts PHP 8.0"
When call validate_input_python "php-version-detect" "default-version" "8.0"
The status should be success
End
It "accepts PHP 8.1"
When call validate_input_python "php-version-detect" "default-version" "8.1"
The status should be success
End
It "accepts PHP 8.4"
When call validate_input_python "php-version-detect" "default-version" "8.4"
The status should be success
End
It "rejects PHP version too old"
When call validate_input_python "php-version-detect" "default-version" "5.6"
The status should be failure
End
It "rejects PHP version too new"
When call validate_input_python "php-version-detect" "default-version" "10.0"
The status should be failure
End
It "rejects invalid version format"
When call validate_input_python "php-version-detect" "default-version" "php8.2"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "php-version-detect" "default-version" "8.2; rm -rf /"
The status should be failure
End
It "rejects version without minor"
When call validate_input_python "php-version-detect" "default-version" "8"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "php-version-detect" "default-version" ""
The status should be failure
End
It "rejects version with v prefix"
When call validate_input_python "php-version-detect" "default-version" "v8.2"
The status should be failure
End
It "accepts PHP 8.5 for future compatibility"
When call validate_input_python "php-version-detect" "default-version" "8.5"
The status should be success
End
It "rejects unreasonably high minor version"
When call validate_input_python "php-version-detect" "default-version" "8.100"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "PHP Version Detect"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "default-version"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "php-version"
End
End
Context "when testing input requirements"
It "has default-version as optional input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "optional"
The output should equal "optional"
End
It "has correct default version"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "default"
The output should equal "8.2"
End
End
Context "when testing security validations"
It "validates against path traversal in version"
When call validate_input_python "php-version-detect" "default-version" "../8.2"
The status should be failure
End
It "validates against shell metacharacters in version"
When call validate_input_python "php-version-detect" "default-version" "8.2|echo"
The status should be failure
End
It "validates against backtick injection"
When call validate_input_python "php-version-detect" "default-version" "8.2\`whoami\`"
The status should be failure
End
It "validates against variable expansion"
When call validate_input_python "php-version-detect" "default-version" "8.2\${HOME}"
The status should be failure
End
End
Context "when testing PHP version range validation"
It "validates PHP 7 minor version boundaries"
When call validate_input_python "php-version-detect" "default-version" "7.0"
The status should be success
End
It "validates PHP 7.4 specifically"
When call validate_input_python "php-version-detect" "default-version" "7.4"
The status should be success
End
It "validates PHP 8 minor version boundaries"
When call validate_input_python "php-version-detect" "default-version" "8.0"
The status should be success
End
It "validates PHP 8.4 boundary"
When call validate_input_python "php-version-detect" "default-version" "8.4"
The status should be success
End
It "validates PHP 9 future version"
When call validate_input_python "php-version-detect" "default-version" "9.0"
The status should be success
End
End
End

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env shellspec
# Unit tests for pr-lint action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "pr-lint action"
ACTION_DIR="pr-lint"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts valid GitHub token"
When call validate_input_python "pr-lint" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "rejects injection in token"
When call validate_input_python "pr-lint" "token" "token; rm -rf /"
The status should be failure
End
End
Context "when validating username input"
It "accepts valid username"
When call validate_input_python "pr-lint" "username" "github-actions"
The status should be success
End
It "rejects injection in username"
When call validate_input_python "pr-lint" "username" "user; rm -rf /"
The status should be failure
End
End
Context "when validating email input"
It "accepts valid email"
When call validate_input_python "pr-lint" "email" "test@example.com"
The status should be success
End
It "rejects invalid email format"
When call validate_input_python "pr-lint" "email" "invalid-email"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "PR Lint"
End
It "defines required inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "token"
The output should include "username"
The output should include "email"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "validation_status"
The output should include "errors_found"
End
End
Context "when validating security"
It "validates token format"
When call validate_input_python "pr-lint" "token" "invalid-token;rm -rf /"
The status should be failure
End
It "validates email format"
When call validate_input_python "pr-lint" "email" "invalid@email"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs"
When call test_action_outputs "$ACTION_DIR" "token" "ghp_test" "username" "test" "email" "test@example.com"
The status should be success
The stderr should include "Testing action outputs for: pr-lint"
The stderr should include "Output test passed for: pr-lint"
End
End
End

View File

@@ -0,0 +1,172 @@
#!/usr/bin/env shellspec
# Unit tests for pre-commit action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "pre-commit action"
ACTION_DIR="pre-commit"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating pre-commit-config input"
It "accepts default config file"
When call validate_input_python "pre-commit" "pre-commit-config" ".pre-commit-config.yaml"
The status should be success
End
It "accepts yml extension"
When call validate_input_python "pre-commit" "pre-commit-config" ".pre-commit-config.yml"
The status should be success
End
# NOTE: Test framework uses default validation for 'pre-commit-config' input
# Default validation only checks for injection patterns (;, &&, $()
It "rejects path traversal"
When call validate_input_python "pre-commit" "pre-commit-config" "../config.yaml"
The status should be failure
End
It "rejects absolute paths"
When call validate_input_python "pre-commit" "pre-commit-config" "/etc/passwd"
The status should be failure
End
It "accepts non-yaml extensions (framework default validation)"
When call validate_input_python "pre-commit" "pre-commit-config" "config.txt"
The status should be success
End
It "rejects injection patterns"
When call validate_input_python "pre-commit" "pre-commit-config" "config.yaml; rm -rf /"
The status should be failure
End
End
Context "when validating base-branch input"
It "accepts valid branch name"
When call validate_input_python "pre-commit" "base-branch" "main"
The status should be success
End
It "accepts feature branch"
When call validate_input_python "pre-commit" "base-branch" "feature/test-branch"
The status should be success
End
It "accepts branch with numbers"
When call validate_input_python "pre-commit" "base-branch" "release-2024.1"
The status should be success
End
It "rejects injection in branch"
When call validate_input_python "pre-commit" "base-branch" "branch; rm -rf /"
The status should be failure
End
# NOTE: Test framework uses default validation for 'base-branch'
# Default validation only checks for injection patterns (;, &&, $()
It "accepts branch with tilde (framework default validation)"
When call validate_input_python "pre-commit" "base-branch" "branch~1"
The status should be success
End
It "accepts branch starting with dot (framework default validation)"
When call validate_input_python "pre-commit" "base-branch" ".hidden-branch"
The status should be success
End
It "rejects injection patterns in branch"
When call validate_input_python "pre-commit" "base-branch" "branch && rm -rf /"
The status should be failure
End
End
Context "when validating token input"
It "accepts valid GitHub token"
When call validate_input_python "pre-commit" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "pre-commit" "token" "invalid-token-format"
The status should be failure
End
It "rejects injection in token"
When call validate_input_python "pre-commit" "token" "token; rm -rf /"
The status should be failure
End
End
Context "when validating commit_user input"
It "accepts valid user"
When call validate_input_python "pre-commit" "commit_user" "GitHub Actions"
The status should be success
End
It "rejects injection in user"
When call validate_input_python "pre-commit" "commit_user" "user; rm -rf /"
The status should be failure
End
End
Context "when validating commit_email input"
It "accepts valid email"
When call validate_input_python "pre-commit" "commit_email" "test@example.com"
The status should be success
End
It "accepts github-actions email"
When call validate_input_python "pre-commit" "commit_email" "github-actions@github.com"
The status should be success
End
It "rejects invalid email format"
When call validate_input_python "pre-commit" "commit_email" "invalid-email"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "pre-commit"
End
It "defines expected inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "pre-commit-config"
The output should include "base-branch"
The output should include "token"
The output should include "commit_user"
The output should include "commit_email"
End
It "has all inputs as optional"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "" "all_optional"
The output should equal "none"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "hooks_passed"
The output should include "files_changed"
End
End
Context "when validating security"
It "rejects path traversal"
When call validate_input_python "pre-commit" "pre-commit-config" "../../malicious.yaml"
The status should be failure
End
It "validates branch name security"
When call validate_input_python "pre-commit" "base-branch" "main && rm -rf /"
The status should be failure
End
It "validates email format"
When call validate_input_python "pre-commit" "commit_email" "invalid@email"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs"
When call test_action_outputs "$ACTION_DIR" "pre-commit-config" ".pre-commit-config.yaml" "token" "ghp_test" "commit_user" "test" "commit_email" "test@example.com"
The status should be success
The stderr should include "Testing action outputs for: pre-commit"
The stderr should include "Output test passed for: pre-commit"
End
End
End

View File

@@ -0,0 +1,332 @@
#!/usr/bin/env shellspec
# Unit tests for prettier-check action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "prettier-check action"
ACTION_DIR="prettier-check"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating working-directory input"
It "accepts current directory"
When call validate_input_python "prettier-check" "working-directory" "."
The status should be success
End
It "accepts relative directory"
When call validate_input_python "prettier-check" "working-directory" "src"
The status should be success
End
It "accepts nested directory"
When call validate_input_python "prettier-check" "working-directory" "src/components"
The status should be success
End
It "rejects path traversal"
When call validate_input_python "prettier-check" "working-directory" "../malicious"
The status should be failure
End
It "rejects absolute paths"
When call validate_input_python "prettier-check" "working-directory" "/etc/passwd"
The status should be failure
End
It "rejects directory with command injection"
When call validate_input_python "prettier-check" "working-directory" "src; rm -rf /"
The status should be failure
End
End
Context "when validating prettier-version input"
It "accepts latest version"
When call validate_input_python "prettier-check" "prettier-version" "latest"
The status should be success
End
It "accepts semantic version"
When call validate_input_python "prettier-check" "prettier-version" "3.0.0"
The status should be success
End
It "accepts prerelease version"
When call validate_input_python "prettier-check" "prettier-version" "3.0.0-alpha"
The status should be success
End
It "rejects invalid version format"
When call validate_input_python "prettier-check" "prettier-version" "v3.0.0"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "prettier-check" "prettier-version" "3.0.0; rm -rf /"
The status should be failure
End
End
Context "when validating config-file input"
It "accepts valid config file"
When call validate_input_python "prettier-check" "config-file" ".prettierrc"
The status should be success
End
It "accepts config file with extension"
When call validate_input_python "prettier-check" "config-file" ".prettierrc.json"
The status should be success
End
It "accepts config file in subdirectory"
When call validate_input_python "prettier-check" "config-file" "config/.prettierrc"
The status should be success
End
It "rejects path traversal in config file"
When call validate_input_python "prettier-check" "config-file" "../../../etc/passwd"
The status should be failure
End
It "rejects absolute path in config file"
When call validate_input_python "prettier-check" "config-file" "/etc/passwd"
The status should be failure
End
End
Context "when validating ignore-file input"
It "accepts valid ignore file"
When call validate_input_python "prettier-check" "ignore-file" ".prettierignore"
The status should be success
End
It "accepts ignore file in subdirectory"
When call validate_input_python "prettier-check" "ignore-file" "config/.prettierignore"
The status should be success
End
It "rejects path traversal in ignore file"
When call validate_input_python "prettier-check" "ignore-file" "../../../etc/passwd"
The status should be failure
End
It "rejects absolute path in ignore file"
When call validate_input_python "prettier-check" "ignore-file" "/etc/passwd"
The status should be failure
End
End
Context "when validating file-pattern input"
It "accepts valid glob pattern"
When call validate_input_python "prettier-check" "file-pattern" "**/*.{js,ts}"
The status should be success
End
It "accepts simple file pattern"
When call validate_input_python "prettier-check" "file-pattern" "*.js"
The status should be success
End
It "accepts multiple extensions"
When call validate_input_python "prettier-check" "file-pattern" "**/*.{js,jsx,ts,tsx,css}"
The status should be success
End
It "rejects pattern with path traversal"
When call validate_input_python "prettier-check" "file-pattern" "../**/*.js"
The status should be failure
End
It "rejects pattern with absolute path"
When call validate_input_python "prettier-check" "file-pattern" "/etc/**/*.conf"
The status should be failure
End
End
Context "when validating boolean inputs"
It "accepts true for cache"
When call validate_input_python "prettier-check" "cache" "true"
The status should be success
End
It "accepts false for cache"
When call validate_input_python "prettier-check" "cache" "false"
The status should be success
End
It "rejects invalid cache value"
When call validate_input_python "prettier-check" "cache" "yes"
The status should be failure
End
It "accepts true for fail-on-error"
When call validate_input_python "prettier-check" "fail-on-error" "true"
The status should be success
End
It "accepts false for fail-on-error"
When call validate_input_python "prettier-check" "fail-on-error" "false"
The status should be success
End
It "accepts true for check-only"
When call validate_input_python "prettier-check" "check-only" "true"
The status should be success
End
It "accepts false for check-only"
When call validate_input_python "prettier-check" "check-only" "false"
The status should be success
End
End
Context "when validating report-format input"
It "accepts json format"
When call validate_input_python "prettier-check" "report-format" "json"
The status should be success
End
It "accepts sarif format"
When call validate_input_python "prettier-check" "report-format" "sarif"
The status should be success
End
It "rejects invalid format"
When call validate_input_python "prettier-check" "report-format" "xml"
The status should be failure
End
It "rejects empty format"
When call validate_input_python "prettier-check" "report-format" ""
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts valid retry count"
When call validate_input_python "prettier-check" "max-retries" "3"
The status should be success
End
It "accepts minimum retries"
When call validate_input_python "prettier-check" "max-retries" "1"
The status should be success
End
It "accepts maximum retries"
When call validate_input_python "prettier-check" "max-retries" "10"
The status should be success
End
It "rejects zero retries"
When call validate_input_python "prettier-check" "max-retries" "0"
The status should be failure
End
It "rejects too many retries"
When call validate_input_python "prettier-check" "max-retries" "11"
The status should be failure
End
It "rejects non-numeric retries"
When call validate_input_python "prettier-check" "max-retries" "many"
The status should be failure
End
End
Context "when validating plugins input"
It "accepts empty plugins"
When call validate_input_python "prettier-check" "plugins" ""
The status should be success
End
It "accepts valid plugin name"
When call validate_input_python "prettier-check" "plugins" "prettier-plugin-java"
The status should be success
End
It "accepts scoped plugin"
When call validate_input_python "prettier-check" "plugins" "@prettier/plugin-xml"
The status should be success
End
It "accepts multiple plugins"
When call validate_input_python "prettier-check" "plugins" "plugin1,@scope/plugin2"
The status should be success
End
It "rejects plugins with command injection"
When call validate_input_python "prettier-check" "plugins" "plugin1; rm -rf /"
The status should be failure
End
It "rejects plugins with shell operators"
When call validate_input_python "prettier-check" "plugins" "plugin1 && malicious"
The status should be failure
End
It "rejects plugins with pipe"
When call validate_input_python "prettier-check" "plugins" "plugin1 | cat /etc/passwd"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Prettier Check"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "working-directory"
The output should include "prettier-version"
The output should include "config-file"
The output should include "ignore-file"
The output should include "file-pattern"
The output should include "cache"
The output should include "fail-on-error"
The output should include "report-format"
The output should include "max-retries"
The output should include "plugins"
The output should include "check-only"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "files-checked"
The output should include "unformatted-files"
The output should include "sarif-file"
The output should include "cache-hit"
End
End
Context "when testing input requirements"
It "has all inputs as optional"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "any" "all_optional"
The output should equal "none"
End
End
Context "when testing security validations"
It "validates against path traversal in multiple inputs"
When call validate_input_python "prettier-check" "working-directory" "../../malicious"
The status should be failure
End
It "validates against command injection in plugins"
When call validate_input_python "prettier-check" "plugins" "plugin\`whoami\`"
The status should be failure
End
It "validates against shell expansion in file patterns"
When call validate_input_python "prettier-check" "file-pattern" "**/*.js\${HOME}"
The status should be failure
End
End
End

View File

@@ -0,0 +1,285 @@
#!/usr/bin/env shellspec
# Unit tests for prettier-fix action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "prettier-fix action"
ACTION_DIR="prettier-fix"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts GitHub token expression"
When call validate_input_python "prettier-fix" "token" "\${{ github.token }}"
The status should be success
End
It "accepts GitHub fine-grained token"
When call validate_input_python "prettier-fix" "token" "ghp_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "accepts GitHub app token"
When call validate_input_python "prettier-fix" "token" "ghs_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "accepts GitHub enterprise token"
When call validate_input_python "prettier-fix" "token" "ghe_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "prettier-fix" "token" "invalid-token"
The status should be failure
End
It "rejects token with command injection"
When call validate_input_python "prettier-fix" "token" "ghp_token; rm -rf /"
The status should be failure
End
It "accepts empty token (uses default)"
When call validate_input_python "prettier-fix" "token" ""
The status should be success
End
End
Context "when validating username input"
It "accepts valid GitHub username"
When call validate_input_python "prettier-fix" "username" "github-actions"
The status should be success
End
It "accepts username with hyphens"
When call validate_input_python "prettier-fix" "username" "user-name"
The status should be success
End
It "accepts username with numbers"
When call validate_input_python "prettier-fix" "username" "user123"
The status should be success
End
It "accepts single character username"
When call validate_input_python "prettier-fix" "username" "a"
The status should be success
End
It "accepts maximum length username"
When call validate_input_python "prettier-fix" "username" "abcdefghijklmnopqrstuvwxyz0123456789abc"
The status should be success
End
It "rejects username too long"
When call validate_input_python "prettier-fix" "username" "abcdefghijklmnopqrstuvwxyz0123456789abcd"
The status should be failure
End
It "rejects username with command injection"
When call validate_input_python "prettier-fix" "username" "user; rm -rf /"
The status should be failure
End
It "rejects username with shell operators"
When call validate_input_python "prettier-fix" "username" "user && rm -rf /"
The status should be failure
End
It "rejects username with pipe"
When call validate_input_python "prettier-fix" "username" "user | rm -rf /"
The status should be failure
End
It "accepts empty username (uses default)"
When call validate_input_python "prettier-fix" "username" ""
The status should be success
End
End
Context "when validating email input"
It "accepts valid email"
When call validate_input_python "prettier-fix" "email" "user@example.com"
The status should be success
End
It "accepts email with subdomain"
When call validate_input_python "prettier-fix" "email" "user@mail.example.com"
The status should be success
End
It "accepts email with plus sign"
When call validate_input_python "prettier-fix" "email" "user+tag@example.com"
The status should be success
End
It "accepts email with numbers"
When call validate_input_python "prettier-fix" "email" "user123@example123.com"
The status should be success
End
It "accepts email with hyphens"
When call validate_input_python "prettier-fix" "email" "user-name@example-domain.com"
The status should be success
End
It "rejects email without at symbol"
When call validate_input_python "prettier-fix" "email" "userexample.com"
The status should be failure
End
It "rejects email without domain"
When call validate_input_python "prettier-fix" "email" "user@"
The status should be failure
End
It "rejects email without username"
When call validate_input_python "prettier-fix" "email" "@example.com"
The status should be failure
End
It "rejects email without dot in domain"
When call validate_input_python "prettier-fix" "email" "user@example"
The status should be failure
End
It "rejects email with spaces"
When call validate_input_python "prettier-fix" "email" "user @example.com"
The status should be failure
End
It "rejects empty email"
When call validate_input_python "prettier-fix" "email" ""
The status should be failure
End
End
Context "when validating max-retries input"
It "accepts valid retry count"
When call validate_input_python "prettier-fix" "max-retries" "3"
The status should be success
End
It "accepts minimum retries"
When call validate_input_python "prettier-fix" "max-retries" "1"
The status should be success
End
It "accepts maximum retries"
When call validate_input_python "prettier-fix" "max-retries" "10"
The status should be success
End
It "rejects zero retries"
When call validate_input_python "prettier-fix" "max-retries" "0"
The status should be failure
End
It "rejects too many retries"
When call validate_input_python "prettier-fix" "max-retries" "11"
The status should be failure
End
It "rejects non-numeric retries"
When call validate_input_python "prettier-fix" "max-retries" "many"
The status should be failure
End
It "rejects negative retries"
When call validate_input_python "prettier-fix" "max-retries" "-1"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Prettier Fix"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "token"
The output should include "username"
The output should include "email"
The output should include "max-retries"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "files_changed"
The output should include "format_status"
End
End
Context "when testing input requirements"
It "has all inputs as optional"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "" "all_optional"
The output should equal "none"
End
It "has correct default token"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "token" "default"
The output should equal "\${{ github.token }}"
End
It "has correct default username"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "username" "default"
The output should equal "github-actions"
End
It "has correct default email"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "email" "default"
The output should equal "github-actions@github.com"
End
End
Context "when testing security validations"
It "validates against command injection in username"
When call validate_input_python "prettier-fix" "username" "user\`whoami\`"
The status should be failure
End
It "validates against shell metacharacters in email"
When call validate_input_python "prettier-fix" "email" "user@example.com; rm -rf /"
The status should be failure
End
It "validates against variable expansion in token"
When call validate_input_python "prettier-fix" "token" "\${MALICIOUS_VAR}"
The status should be failure
End
It "validates against backtick injection in email"
When call validate_input_python "prettier-fix" "email" "user@example.com\`echo test\`"
The status should be failure
End
End
Context "when testing Prettier-specific validations"
It "validates username length boundaries for Git"
When call validate_input_python "prettier-fix" "username" "$(awk 'BEGIN{for(i=1;i<=40;i++)printf "a"}')"
The status should be failure
End
It "validates email format for Git commits"
When call validate_input_python "prettier-fix" "email" "noreply@github.com"
The status should be success
End
It "validates retry count boundaries"
When call validate_input_python "prettier-fix" "max-retries" "0"
The status should be failure
End
It "validates default values are secure"
When call validate_input_python "prettier-fix" "username" "github-actions"
The status should be success
End
End
End

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env shellspec
# Unit tests for python-lint-fix action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "python-lint-fix action"
ACTION_DIR="python-lint-fix"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts GitHub token expression"
When call validate_input_python "python-lint-fix" "token" "\${{ github.token }}"
The status should be success
End
It "accepts GitHub fine-grained token"
When call validate_input_python "python-lint-fix" "token" "ghp_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "accepts GitHub app token"
When call validate_input_python "python-lint-fix" "token" "ghs_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "python-lint-fix" "token" "invalid-token"
The status should be failure
End
It "rejects token with command injection"
When call validate_input_python "python-lint-fix" "token" "ghp_token; rm -rf /"
The status should be failure
End
It "accepts empty token (uses default)"
When call validate_input_python "python-lint-fix" "token" ""
The status should be success
End
End
Context "when validating username input"
It "accepts valid GitHub username"
When call validate_input_python "python-lint-fix" "username" "github-actions"
The status should be success
End
It "accepts username with hyphens"
When call validate_input_python "python-lint-fix" "username" "user-name"
The status should be success
End
It "accepts username with numbers"
When call validate_input_python "python-lint-fix" "username" "user123"
The status should be success
End
It "rejects username too long"
When call validate_input_python "python-lint-fix" "username" "$(awk 'BEGIN{for(i=1;i<=40;i++)printf "a"}')"
The status should be failure
End
It "rejects username with command injection"
When call validate_input_python "python-lint-fix" "username" "user; rm -rf /"
The status should be failure
End
It "accepts empty username (uses default)"
When call validate_input_python "python-lint-fix" "username" ""
The status should be success
End
End
Context "when validating email input"
It "accepts valid email"
When call validate_input_python "python-lint-fix" "email" "user@example.com"
The status should be success
End
It "accepts email with subdomain"
When call validate_input_python "python-lint-fix" "email" "user@mail.example.com"
The status should be success
End
It "rejects email without at symbol"
When call validate_input_python "python-lint-fix" "email" "userexample.com"
The status should be failure
End
It "rejects email without domain"
When call validate_input_python "python-lint-fix" "email" "user@"
The status should be failure
End
It "rejects email with spaces"
When call validate_input_python "python-lint-fix" "email" "user @example.com"
The status should be failure
End
It "accepts empty email (uses default)"
When call uv run "_tests/shared/validation_core.py" --validate "python-lint-fix" "email" ""
The status should be success
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Python Lint and Fix"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "token"
The output should include "username"
The output should include "email"
End
End
Context "when testing input requirements"
It "has all inputs as optional"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "" "all_optional"
The output should equal "none"
End
End
Context "when testing security validations"
It "validates against command injection in username"
When call validate_input_python "python-lint-fix" "username" "user\`whoami\`"
The status should be failure
End
It "validates against shell metacharacters in email"
When call validate_input_python "python-lint-fix" "email" "user@example.com; rm -rf /"
The status should be failure
End
It "validates against variable expansion in token"
When call validate_input_python "python-lint-fix" "token" "\${MALICIOUS_VAR}"
The status should be failure
End
End
End

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env shellspec
# Unit tests for python-version-detect-v2 action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "python-version-detect-v2 action"
ACTION_DIR="python-version-detect-v2"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating default-version input"
It "accepts valid Python version"
When call validate_input_python "python-version-detect-v2" "default-version" "3.11"
The status should be success
End
It "accepts Python version with patch"
When call validate_input_python "python-version-detect-v2" "default-version" "3.11.5"
The status should be success
End
It "accepts Python 3.8"
When call validate_input_python "python-version-detect-v2" "default-version" "3.8"
The status should be success
End
It "accepts Python 3.12"
When call validate_input_python "python-version-detect-v2" "default-version" "3.12"
The status should be success
End
It "rejects Python version too old"
When call validate_input_python "python-version-detect-v2" "default-version" "2.7"
The status should be failure
End
It "rejects invalid version format"
When call validate_input_python "python-version-detect-v2" "default-version" "python3.11"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "python-version-detect-v2" "default-version" "3.11; rm -rf /"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "python-version-detect-v2" "default-version" ""
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Python Version Detect v2"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "default-version"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "python-version"
End
End
Context "when testing input requirements"
It "has default-version as optional input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "optional"
The output should equal "optional"
End
End
Context "when testing security validations"
It "validates against path traversal in version"
When call validate_input_python "python-version-detect-v2" "default-version" "../3.11"
The status should be failure
End
It "validates against shell metacharacters in version"
When call validate_input_python "python-version-detect-v2" "default-version" "3.11|echo"
The status should be failure
End
It "validates against backtick injection"
When call validate_input_python "python-version-detect-v2" "default-version" "3.11\`whoami\`"
The status should be failure
End
End
End

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env shellspec
# Unit tests for python-version-detect action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "python-version-detect action"
ACTION_DIR="python-version-detect"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating default-version input"
It "accepts valid Python version"
When call validate_input_python "python-version-detect" "default-version" "3.11"
The status should be success
End
It "accepts Python version with patch"
When call validate_input_python "python-version-detect" "default-version" "3.11.5"
The status should be success
End
It "accepts Python 3.8"
When call validate_input_python "python-version-detect" "default-version" "3.8"
The status should be success
End
It "accepts Python 3.12"
When call validate_input_python "python-version-detect" "default-version" "3.12"
The status should be success
End
It "rejects Python version too old"
When call validate_input_python "python-version-detect" "default-version" "2.7"
The status should be failure
End
It "rejects Python version too new"
When call validate_input_python "python-version-detect" "default-version" "4.0"
The status should be failure
End
It "rejects invalid version format"
When call validate_input_python "python-version-detect" "default-version" "python3.11"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "python-version-detect" "default-version" "3.11; rm -rf /"
The status should be failure
End
It "rejects version without minor"
When call validate_input_python "python-version-detect" "default-version" "3"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "python-version-detect" "default-version" ""
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Python Version Detect"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "default-version"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "python-version"
End
End
Context "when testing input requirements"
It "has default-version as optional input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "default-version" "optional"
The output should equal "optional"
End
End
Context "when testing security validations"
It "validates against path traversal in version"
When call validate_input_python "python-version-detect" "default-version" "../3.11"
The status should be failure
End
It "validates against shell metacharacters in version"
When call validate_input_python "python-version-detect" "default-version" "3.11|echo"
The status should be failure
End
It "validates against backtick injection"
When call validate_input_python "python-version-detect" "default-version" "3.11\`whoami\`"
The status should be failure
End
End
End

View File

@@ -0,0 +1,125 @@
#!/usr/bin/env shellspec
# Unit tests for release-monthly action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "release-monthly action"
ACTION_DIR="release-monthly"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
# NOTE: Test framework uses strict GitHub token format validation
It "accepts valid GitHub token with correct format"
When call validate_input_python "release-monthly" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "rejects empty token"
When call validate_input_python "release-monthly" "token" ""
The status should be failure
End
It "rejects injection in token"
When call validate_input_python "release-monthly" "token" "token; rm -rf /"
The status should be failure
End
End
Context "when validating dry-run input"
It "accepts true value"
When call validate_input_python "release-monthly" "dry-run" "true"
The status should be success
End
It "accepts false value"
When call validate_input_python "release-monthly" "dry-run" "false"
The status should be success
End
# NOTE: Convention-based validation applies boolean validation to 'dry-run'
# Boolean validator rejects non-boolean values
It "rejects invalid boolean value"
When call validate_input_python "release-monthly" "dry-run" "maybe"
The status should be failure
End
It "rejects injection in dry-run"
When call validate_input_python "release-monthly" "dry-run" "true; rm -rf /"
The status should be failure
End
End
Context "when validating prefix input"
# NOTE: prefix has default: '' so empty values are accepted
It "accepts empty prefix (has empty default)"
When call validate_input_python "release-monthly" "prefix" ""
The status should be success
End
It "accepts valid prefix"
When call validate_input_python "release-monthly" "prefix" "v"
The status should be success
End
It "accepts alphanumeric prefix"
When call validate_input_python "release-monthly" "prefix" "release-v1.0-"
The status should be success
End
# NOTE: Test framework uses default validation for 'prefix'
# Default validation only checks injection patterns, not character restrictions
It "accepts special characters in prefix (framework default validation)"
When call validate_input_python "release-monthly" "prefix" "invalid@prefix"
The status should be success
End
It "accepts spaces in prefix (framework default validation)"
When call validate_input_python "release-monthly" "prefix" "invalid prefix"
The status should be success
End
It "rejects injection in prefix"
When call validate_input_python "release-monthly" "prefix" "prefix; rm -rf /"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Do Monthly Release"
End
It "defines required inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "token"
The output should include "dry-run"
The output should include "prefix"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "release-tag"
The output should include "release-url"
The output should include "previous-tag"
End
End
Context "when validating security"
It "validates token is required"
When call validate_input_python "release-monthly" "token" ""
The status should be failure
End
It "validates prefix format"
When call validate_input_python "release-monthly" "prefix" "invalid;prefix"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs"
When call test_action_outputs "$ACTION_DIR" "token" "ghp_test" "dry-run" "true" "prefix" "v"
The status should be success
The stderr should include "Testing action outputs for: release-monthly"
The stderr should include "Output test passed for: release-monthly"
End
End
End

View File

@@ -0,0 +1,69 @@
#!/usr/bin/env shellspec
# Unit tests for set-git-config action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "set-git-config action"
ACTION_DIR="set-git-config"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating inputs (no validation logic in action)"
# NOTE: This action has no validation logic - all inputs are accepted
# The action simply passes through values and conditionally sets outputs
It "accepts valid token value"
When call validate_input_python "set-git-config" "token" "ghp_123456789012345678901234567890123456"
The status should be success
End
It "accepts any username value"
When call validate_input_python "set-git-config" "username" "any-username"
The status should be success
End
It "accepts valid email value"
When call validate_input_python "set-git-config" "email" "test@example.com"
The status should be success
End
It "accepts any is_fiximus value"
When call validate_input_python "set-git-config" "is_fiximus" "any-value"
The status should be success
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Set Git Config"
End
It "defines required inputs"
inputs=$(get_action_inputs "$ACTION_FILE")
When call echo "$inputs"
The output should include "token"
The output should include "username"
The output should include "email"
The output should include "is_fiximus"
End
It "defines expected outputs"
outputs=$(get_action_outputs "$ACTION_FILE")
When call echo "$outputs"
The output should include "token"
The output should include "username"
The output should include "email"
The output should include "is_fiximus"
End
End
Context "when testing outputs"
It "produces all expected outputs"
When call test_action_outputs "$ACTION_DIR" "token" "ghp_test" "username" "test" "email" "test@example.com" "is_fiximus" "false"
The status should be success
The stderr should include "Testing action outputs for: set-git-config"
The stderr should include "Output test passed for: set-git-config"
End
End
End

579
_tests/unit/spec_helper.sh Executable file
View File

@@ -0,0 +1,579 @@
#!/usr/bin/env bash
# ShellSpec spec helper for GitHub Actions Testing Framework
# This file is automatically loaded by ShellSpec for all tests
set -euo pipefail
# Get the project root directory (where .shellspec is located)
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# Test framework directories
TEST_ROOT="${PROJECT_ROOT}/_tests"
FRAMEWORK_DIR="${TEST_ROOT}/framework"
FIXTURES_DIR="${FRAMEWORK_DIR}/fixtures"
MOCKS_DIR="${FRAMEWORK_DIR}/mocks"
# Export directories for use by test cases
export FIXTURES_DIR MOCKS_DIR
# Only create TEMP_DIR if not already set (framework setup.sh will create it)
if [ -z "${TEMP_DIR:-}" ]; then
TEMP_DIR=$(mktemp -d) || exit 1
fi
# Load framework utilities
# shellcheck source=_tests/framework/setup.sh
source "${FRAMEWORK_DIR}/setup.sh"
# shellcheck source=_tests/framework/utils.sh
source "${FRAMEWORK_DIR}/utils.sh"
# Initialize testing framework
init_testing_framework
# ShellSpec specific setup
spec_helper_configure() {
# Configure ShellSpec behavior
# Set up environment variables for tests
export GITHUB_ACTIONS=true
export GITHUB_WORKSPACE="${PROJECT_ROOT}"
export GITHUB_REPOSITORY="ivuorinen/actions"
export GITHUB_SHA="test-sha"
export GITHUB_REF="refs/heads/main"
export GITHUB_TOKEN="test-token"
# Temporary directory already created by mktemp above
# Set up default GITHUB_OUTPUT if not already set
if [[ -z ${GITHUB_OUTPUT:-} ]]; then
export GITHUB_OUTPUT="${TEMP_DIR}/default-github-output"
touch "$GITHUB_OUTPUT"
fi
# Quiet logging during ShellSpec runs to avoid output interference
if [[ -z ${SHELLSPEC_VERSION:-} ]]; then
log_info "ShellSpec helper configured - framework loaded"
fi
}
# Run configuration
spec_helper_configure
# Helper functions specifically for ShellSpec tests
# Set up default input values for testing a single input
# This prevents validation failures when testing one input at a time
setup_default_inputs() {
local action_name="$1"
local input_name="$2"
case "$action_name" in
"github-release")
[[ "$input_name" != "version" ]] && export INPUT_VERSION="1.0.0"
;;
"docker-build" | "docker-publish" | "docker-publish-gh" | "docker-publish-hub")
[[ "$input_name" != "image-name" ]] && export INPUT_IMAGE_NAME="test-image"
[[ "$input_name" != "tag" ]] && export INPUT_TAG="latest"
[[ "$action_name" == "docker-publish" && "$input_name" != "registry" ]] && export INPUT_REGISTRY="dockerhub"
;;
"npm-publish")
[[ "$input_name" != "npm_token" ]] && export INPUT_NPM_TOKEN="ghp_123456789012345678901234567890123456"
;;
"csharp-publish")
[[ "$input_name" != "token" ]] && export INPUT_TOKEN="ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
[[ "$input_name" != "version" ]] && export INPUT_VERSION="1.0.0"
[[ "$input_name" != "namespace" ]] && export INPUT_NAMESPACE="test-namespace"
;;
"php-composer")
[[ "$input_name" != "php" ]] && export INPUT_PHP="8.1"
;;
"php-tests" | "php-laravel-phpunit")
[[ "$input_name" != "php-version" ]] && export INPUT_PHP_VERSION="8.1"
;;
"go-build" | "go-lint")
[[ "$input_name" != "go-version" ]] && export INPUT_GO_VERSION="1.21"
;;
"common-cache")
[[ "$input_name" != "type" ]] && export INPUT_TYPE="npm"
[[ "$input_name" != "paths" ]] && export INPUT_PATHS="node_modules"
;;
"common-retry")
[[ "$input_name" != "command" ]] && export INPUT_COMMAND="echo test"
;;
"dotnet-version-detect")
[[ "$input_name" != "default-version" ]] && export INPUT_DEFAULT_VERSION="8.0"
;;
"python-version-detect" | "python-version-detect-v2")
[[ "$input_name" != "default-version" ]] && export INPUT_DEFAULT_VERSION="3.11"
;;
"php-version-detect")
[[ "$input_name" != "default-version" ]] && export INPUT_DEFAULT_VERSION="8.1"
;;
"go-version-detect")
[[ "$input_name" != "default-version" ]] && export INPUT_DEFAULT_VERSION="1.22"
;;
"validate-inputs")
[[ "$input_name" != "action-type" && "$input_name" != "action" && "$input_name" != "rules-file" && "$input_name" != "fail-on-error" ]] && export INPUT_ACTION_TYPE="test-action"
;;
"version-file-parser")
[[ "$input_name" != "language" ]] && export INPUT_LANGUAGE="node"
[[ "$input_name" != "tool-versions-key" ]] && export INPUT_TOOL_VERSIONS_KEY="nodejs"
[[ "$input_name" != "dockerfile-image" ]] && export INPUT_DOCKERFILE_IMAGE="node"
;;
"codeql-analysis")
[[ "$input_name" != "language" ]] && export INPUT_LANGUAGE="javascript"
[[ "$input_name" != "token" ]] && export INPUT_TOKEN="ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
;;
"version-validator")
[[ "$input_name" != "version" ]] && export INPUT_VERSION="1.0.0"
;;
"release-monthly")
[[ "$input_name" != "token" ]] && export INPUT_TOKEN="ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
;;
esac
}
# Clean up default input values after testing
cleanup_default_inputs() {
local action_name="$1"
local input_name="$2"
case "$action_name" in
"github-release")
[[ "$input_name" != "version" ]] && unset INPUT_VERSION
;;
"docker-build" | "docker-publish" | "docker-publish-gh" | "docker-publish-hub")
[[ "$input_name" != "image-name" ]] && unset INPUT_IMAGE_NAME
[[ "$input_name" != "tag" ]] && unset INPUT_TAG
[[ "$action_name" == "docker-publish" && "$input_name" != "registry" ]] && unset INPUT_REGISTRY
;;
"npm-publish")
[[ "$input_name" != "npm_token" ]] && unset INPUT_NPM_TOKEN
;;
"csharp-publish")
[[ "$input_name" != "token" ]] && unset INPUT_TOKEN
[[ "$input_name" != "version" ]] && unset INPUT_VERSION
[[ "$input_name" != "namespace" ]] && unset INPUT_NAMESPACE
;;
"php-composer")
[[ "$input_name" != "php" ]] && unset INPUT_PHP
;;
"php-tests" | "php-laravel-phpunit")
[[ "$input_name" != "php-version" ]] && unset INPUT_PHP_VERSION
;;
"go-build" | "go-lint")
[[ "$input_name" != "go-version" ]] && unset INPUT_GO_VERSION
;;
"common-cache")
[[ "$input_name" != "type" ]] && unset INPUT_TYPE
[[ "$input_name" != "paths" ]] && unset INPUT_PATHS
;;
"common-retry")
[[ "$input_name" != "command" ]] && unset INPUT_COMMAND
;;
"dotnet-version-detect")
[[ "$input_name" != "default-version" ]] && unset INPUT_DEFAULT_VERSION
;;
"python-version-detect" | "python-version-detect-v2")
[[ "$input_name" != "default-version" ]] && unset INPUT_DEFAULT_VERSION
;;
"php-version-detect")
[[ "$input_name" != "default-version" ]] && unset INPUT_DEFAULT_VERSION
;;
"go-version-detect")
[[ "$input_name" != "default-version" ]] && unset INPUT_DEFAULT_VERSION
;;
"validate-inputs")
[[ "$input_name" != "action-type" && "$input_name" != "action" && "$input_name" != "rules-file" && "$input_name" != "fail-on-error" ]] && unset INPUT_ACTION_TYPE
;;
"version-file-parser")
[[ "$input_name" != "language" ]] && unset INPUT_LANGUAGE
[[ "$input_name" != "tool-versions-key" ]] && unset INPUT_TOOL_VERSIONS_KEY
[[ "$input_name" != "dockerfile-image" ]] && unset INPUT_DOCKERFILE_IMAGE
;;
"codeql-analysis")
[[ "$input_name" != "language" ]] && unset INPUT_LANGUAGE
[[ "$input_name" != "token" ]] && unset INPUT_TOKEN
;;
"version-validator")
[[ "$input_name" != "version" ]] && unset INPUT_VERSION
;;
"release-monthly")
[[ "$input_name" != "token" ]] && unset INPUT_TOKEN
;;
esac
}
# Enhanced test validation for ShellSpec
shellspec_validate_action_output() {
local expected_key="$1"
local expected_value="$2"
local output_file="${3:-$GITHUB_OUTPUT}"
if [[ ! -f $output_file ]]; then
echo "Output file not found: $output_file" >&2
return 1
fi
if grep -Fq "${expected_key}=${expected_value}" "$output_file"; then
return 0
else
echo "Expected output not found: $expected_key=$expected_value" >&2
echo "Actual outputs:" >&2
cat "$output_file" >&2
return 1
fi
}
# Mock action execution for ShellSpec tests
shellspec_mock_action_run() {
local action_dir="$1"
shift
# Set up inputs as environment variables
while [[ $# -gt 1 ]]; do
local key="$1"
local value="$2"
# Convert dashes to underscores for environment variable names
local env_key="${key//-/_}"
export "INPUT_$(echo "$env_key" | tr '[:lower:]' '[:upper:]')"="$value"
shift 2
done
# For testing, we'll simulate action outputs based on the action type
local action_name
action_name=$(basename "$action_dir")
case "$action_name" in
"version-file-parser")
echo "detected-version=1.0.0" >>"$GITHUB_OUTPUT"
echo "package-manager=npm" >>"$GITHUB_OUTPUT"
;;
"node-setup")
echo "node-version=18.0.0" >>"$GITHUB_OUTPUT"
echo "package-manager=npm" >>"$GITHUB_OUTPUT"
echo "cache-hit=false" >>"$GITHUB_OUTPUT"
;;
"docker-build")
echo "image-digest=sha256:abc123" >>"$GITHUB_OUTPUT"
echo "build-time=45" >>"$GITHUB_OUTPUT"
echo "platforms=linux/amd64" >>"$GITHUB_OUTPUT"
;;
"common-cache")
echo "cache-hit=true" >>"$GITHUB_OUTPUT"
echo "cache-key=Linux-npm-abc123" >>"$GITHUB_OUTPUT"
echo "cache-paths=node_modules" >>"$GITHUB_OUTPUT"
;;
"common-file-check")
echo "found=true" >>"$GITHUB_OUTPUT"
;;
"common-retry")
echo "success=true" >>"$GITHUB_OUTPUT"
echo "attempts=1" >>"$GITHUB_OUTPUT"
echo "exit-code=0" >>"$GITHUB_OUTPUT"
echo "duration=5" >>"$GITHUB_OUTPUT"
;;
"compress-images")
echo "images_compressed=true" >>"$GITHUB_OUTPUT"
printf "compression_report=## Compression Results\n- 3 images compressed\n- 25%% size reduction\n" >>"$GITHUB_OUTPUT"
;;
"csharp-build")
echo "build_status=success" >>"$GITHUB_OUTPUT"
echo "test_status=success" >>"$GITHUB_OUTPUT"
echo "dotnet_version=7.0" >>"$GITHUB_OUTPUT"
echo "artifacts_path=**/bin/Release/**/*" >>"$GITHUB_OUTPUT"
echo "test_results_path=**/*.trx" >>"$GITHUB_OUTPUT"
;;
"csharp-lint-check")
echo "lint_status=success" >>"$GITHUB_OUTPUT"
echo "errors_count=0" >>"$GITHUB_OUTPUT"
echo "warnings_count=0" >>"$GITHUB_OUTPUT"
;;
"csharp-publish")
echo "publish_status=success" >>"$GITHUB_OUTPUT"
echo "package_version=1.2.3" >>"$GITHUB_OUTPUT"
echo "package_url=https://github.com/ivuorinen/packages/nuget" >>"$GITHUB_OUTPUT"
;;
"docker-publish")
echo "registry=github,dockerhub" >>"$GITHUB_OUTPUT"
echo "tags=latest,v1.2.3" >>"$GITHUB_OUTPUT"
echo "build-time=120" >>"$GITHUB_OUTPUT"
echo 'platform-matrix={"linux/amd64":"success","linux/arm64":"success"}' >>"$GITHUB_OUTPUT"
echo 'scan-results={"vulnerabilities":0}' >>"$GITHUB_OUTPUT"
;;
"docker-publish-gh")
echo "image-name=ghcr.io/ivuorinen/test" >>"$GITHUB_OUTPUT"
echo "digest=sha256:abc123def456" >>"$GITHUB_OUTPUT"
echo "tags=ghcr.io/ivuorinen/test:latest,ghcr.io/ivuorinen/test:v1.2.3" >>"$GITHUB_OUTPUT"
echo "provenance=true" >>"$GITHUB_OUTPUT"
echo "sbom=ghcr.io/ivuorinen/test.sbom" >>"$GITHUB_OUTPUT"
echo 'scan-results={"vulnerabilities":0,"critical":0}' >>"$GITHUB_OUTPUT"
echo 'platform-matrix={"linux/amd64":"success","linux/arm64":"success"}' >>"$GITHUB_OUTPUT"
echo "build-time=180" >>"$GITHUB_OUTPUT"
;;
"docker-publish-hub")
echo "image-name=ivuorinen/test-app" >>"$GITHUB_OUTPUT"
echo "digest=sha256:hub123def456" >>"$GITHUB_OUTPUT"
echo "tags=ivuorinen/test-app:latest,ivuorinen/test-app:v1.2.3" >>"$GITHUB_OUTPUT"
echo "repo-url=https://hub.docker.com/r/ivuorinen/test-app" >>"$GITHUB_OUTPUT"
echo 'scan-results={"vulnerabilities":2,"critical":0}' >>"$GITHUB_OUTPUT"
echo 'platform-matrix={"linux/amd64":"success","linux/arm64":"success"}' >>"$GITHUB_OUTPUT"
echo "build-time=240" >>"$GITHUB_OUTPUT"
echo "signature=signed" >>"$GITHUB_OUTPUT"
;;
"dotnet-version-detect")
echo "dotnet-version=7.0.403" >>"$GITHUB_OUTPUT"
;;
"eslint-check")
echo "error-count=0" >>"$GITHUB_OUTPUT"
echo "warning-count=3" >>"$GITHUB_OUTPUT"
echo "sarif-file=reports/eslint.sarif" >>"$GITHUB_OUTPUT"
echo "files-checked=15" >>"$GITHUB_OUTPUT"
;;
"eslint-fix")
echo "fixed-count=5" >>"$GITHUB_OUTPUT"
echo "files-fixed=3" >>"$GITHUB_OUTPUT"
echo "error-count=0" >>"$GITHUB_OUTPUT"
echo "warning-count=0" >>"$GITHUB_OUTPUT"
;;
"github-release")
echo "release-id=123456789" >>"$GITHUB_OUTPUT"
echo "release-url=https://github.com/ivuorinen/test/releases/tag/v1.2.3" >>"$GITHUB_OUTPUT"
echo "asset-urls=https://github.com/ivuorinen/test/releases/download/v1.2.3/app.tar.gz" >>"$GITHUB_OUTPUT"
echo "tag-name=v1.2.3" >>"$GITHUB_OUTPUT"
;;
"go-build")
echo "build_status=success" >>"$GITHUB_OUTPUT"
echo "test_status=success" >>"$GITHUB_OUTPUT"
echo "go_version=1.21.5" >>"$GITHUB_OUTPUT"
echo "binary_path=./bin" >>"$GITHUB_OUTPUT"
echo "coverage_path=coverage.out" >>"$GITHUB_OUTPUT"
;;
"go-lint")
echo "lint_status=success" >>"$GITHUB_OUTPUT"
echo "issues_count=0" >>"$GITHUB_OUTPUT"
echo "files_checked=25" >>"$GITHUB_OUTPUT"
echo "golangci_version=1.55.2" >>"$GITHUB_OUTPUT"
;;
"go-version-detect")
echo "go-version=1.21" >>"$GITHUB_OUTPUT"
;;
"npm-publish")
echo "publish-status=success" >>"$GITHUB_OUTPUT"
echo "package-version=1.2.3" >>"$GITHUB_OUTPUT"
echo "registry-url=https://registry.npmjs.org" >>"$GITHUB_OUTPUT"
echo "package-url=https://www.npmjs.com/package/test-package" >>"$GITHUB_OUTPUT"
;;
"php-composer")
echo "composer-version=2.6.5" >>"$GITHUB_OUTPUT"
echo "install-status=success" >>"$GITHUB_OUTPUT"
echo "dependencies-count=15" >>"$GITHUB_OUTPUT"
echo "php-version=8.2.0" >>"$GITHUB_OUTPUT"
echo "lock-file-updated=false" >>"$GITHUB_OUTPUT"
;;
*)
# Generic mock outputs
echo "status=success" >>"$GITHUB_OUTPUT"
;;
esac
}
# Use centralized Python validation system for input validation testing
shellspec_test_input_validation() {
local action_dir="$1"
local input_name="$2"
local test_value="$3"
local expected_result="${4:-success}"
# Get the action name from the directory
local action_name
action_name=$(basename "$action_dir")
# Set up environment for Python validation
local temp_output_file
temp_output_file=$(mktemp)
# Capture original INPUT_ACTION_TYPE state to restore after test
local original_action_type_set=false
local original_action_type_value=""
if [[ -n "${INPUT_ACTION_TYPE+x}" ]]; then
original_action_type_set=true
original_action_type_value="$INPUT_ACTION_TYPE"
fi
# Set environment variables for the validation script
# Only set INPUT_ACTION_TYPE if we're not testing the action input
if [[ "$input_name" != "action" ]]; then
export INPUT_ACTION_TYPE="$action_name"
fi
# Set default values for commonly required inputs to avoid validation failures
# when testing only one input at a time
setup_default_inputs "$action_name" "$input_name"
# Convert input name to uppercase and replace dashes with underscores
local input_var_name
input_var_name="INPUT_${input_name//-/_}"
input_var_name="$(echo "$input_var_name" | tr '[:lower:]' '[:upper:]')"
export "$input_var_name"="$test_value"
export GITHUB_OUTPUT="$temp_output_file"
# Run the Python validation script and capture exit code
local exit_code
if python3 "${PROJECT_ROOT}/validate-inputs/validator.py" >/dev/null 2>&1; then
exit_code=0
else
exit_code=1
fi
# Determine the actual result based on exit code
local actual_result
if [[ $exit_code -eq 0 ]]; then
actual_result="success"
else
actual_result="failure"
fi
# Clean up
rm -f "$temp_output_file" 2>/dev/null || true
unset "$input_var_name"
# Clean up default inputs
cleanup_default_inputs "$action_name" "$input_name"
# Restore original INPUT_ACTION_TYPE state
if [[ "$original_action_type_set" == "true" ]]; then
export INPUT_ACTION_TYPE="$original_action_type_value"
else
unset INPUT_ACTION_TYPE
fi
# Return based on expected result
if [[ $actual_result == "$expected_result" ]]; then
return 0
else
return 1
fi
}
# Test environment setup that works with ShellSpec
shellspec_setup_test_env() {
local test_name="${1:-shellspec-test}"
# Create unique temporary directory for this test
export SHELLSPEC_TEST_TEMP_DIR="${TEMP_DIR}/${test_name}-$$"
mkdir -p "$SHELLSPEC_TEST_TEMP_DIR"
# Create fake GitHub workspace
export SHELLSPEC_TEST_WORKSPACE="${SHELLSPEC_TEST_TEMP_DIR}/workspace"
mkdir -p "$SHELLSPEC_TEST_WORKSPACE"
# Setup fake GitHub outputs
export GITHUB_OUTPUT="${SHELLSPEC_TEST_TEMP_DIR}/github-output"
export GITHUB_ENV="${SHELLSPEC_TEST_TEMP_DIR}/github-env"
export GITHUB_PATH="${SHELLSPEC_TEST_TEMP_DIR}/github-path"
export GITHUB_STEP_SUMMARY="${SHELLSPEC_TEST_TEMP_DIR}/github-step-summary"
# Initialize output files
touch "$GITHUB_OUTPUT" "$GITHUB_ENV" "$GITHUB_PATH" "$GITHUB_STEP_SUMMARY"
# Change to test workspace
cd "$SHELLSPEC_TEST_WORKSPACE"
}
# Test environment cleanup for ShellSpec
shellspec_cleanup_test_env() {
local test_name="${1:-shellspec-test}"
if [[ -n ${SHELLSPEC_TEST_TEMP_DIR:-} && -d $SHELLSPEC_TEST_TEMP_DIR ]]; then
rm -rf "$SHELLSPEC_TEST_TEMP_DIR"
fi
# Return to project root
cd "$PROJECT_ROOT"
}
# Export functions for use in specs
export -f shellspec_validate_action_output shellspec_mock_action_run
export -f shellspec_setup_test_env shellspec_cleanup_test_env shellspec_test_input_validation
# Create alias for backward compatibility (override framework version)
test_input_validation() {
shellspec_test_input_validation "$@"
}
# Export all framework functions for backward compatibility
export -f setup_test_env cleanup_test_env create_mock_repo
export -f create_mock_node_repo
export -f validate_action_output check_required_tools
export -f log_info log_success log_warning log_error
export -f validate_action_yml get_action_inputs get_action_outputs get_action_name
export -f test_action_outputs test_external_usage test_input_validation
# Quiet wrapper for validate_action_yml in tests
validate_action_yml_quiet() {
validate_action_yml "$1" "true"
}
# =============================================================================
# VALIDATION TEST HELPERS
# =============================================================================
# Note: These helpers return validation results but cannot use ShellSpec commands
# They must be called from within ShellSpec It blocks
# Modern Python-based validation function for direct testing
validate_input_python() {
local action_type="$1"
local input_name="$2"
local input_value="$3"
# Set up environment variables for Python validator
export INPUT_ACTION_TYPE="$action_type"
export VALIDATOR_QUIET="1" # Suppress success messages for tests
# Set default values for commonly required inputs to avoid validation failures
# when testing only one input at a time
setup_default_inputs "$action_type" "$input_name"
# Set the target input
local input_var_name="INPUT_${input_name//-/_}"
input_var_name="$(echo "$input_var_name" | tr '[:lower:]' '[:upper:]')"
export "$input_var_name"="$input_value"
# Set up GitHub output file
local temp_output
temp_output=$(mktemp)
export GITHUB_OUTPUT="$temp_output"
# Call Python validator directly
if [[ "${SHELLSPEC_DEBUG:-}" == "1" ]]; then
echo "DEBUG: Testing $action_type $input_name=$input_value"
echo "DEBUG: Environment variables:"
env | grep "^INPUT_" | sort
fi
# Run validator and output everything to stdout for ShellSpec
uv run "${PROJECT_ROOT}/validate-inputs/validator.py" 2>&1
local exit_code=$?
# Clean up target input
unset INPUT_ACTION_TYPE "$input_var_name" GITHUB_OUTPUT VALIDATOR_QUIET
rm -f "$temp_output" 2>/dev/null || true
# Clean up default inputs
cleanup_default_inputs "$action_type" "$input_name"
# Return the exit code for ShellSpec to check
return $exit_code
}
# Export all new simplified helpers (functions are moved above)
export -f validate_action_yml_quiet validate_input_python
# Removed EXIT trap setup to avoid conflicts with ShellSpec
# ShellSpec handles its own cleanup, and our framework cleanup is handled in setup.sh
# Quiet logging during ShellSpec runs
if [[ -z ${SHELLSPEC_VERSION:-} ]]; then
log_success "ShellSpec spec helper loaded successfully"
fi

View File

@@ -0,0 +1,139 @@
#!/usr/bin/env shellspec
# Unit tests for stale action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "stale action"
ACTION_DIR="stale"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts GitHub token expression"
When call validate_input_python "stale" "token" "\${{ github.token }}"
The status should be success
End
It "accepts GitHub fine-grained token"
When call validate_input_python "stale" "token" "ghp_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "stale" "token" "invalid-token"
The status should be failure
End
It "rejects token with command injection"
When call validate_input_python "stale" "token" "ghp_token; rm -rf /"
The status should be failure
End
It "accepts empty token (uses default)"
When call validate_input_python "stale" "token" ""
The status should be success
End
End
Context "when validating days-before-stale input"
It "accepts valid day count"
When call validate_input_python "stale" "days-before-stale" "30"
The status should be success
End
It "accepts minimum days"
When call validate_input_python "stale" "days-before-stale" "1"
The status should be success
End
It "accepts reasonable maximum days"
When call validate_input_python "stale" "days-before-stale" "365"
The status should be success
End
It "rejects zero days"
When call validate_input_python "stale" "days-before-stale" "0"
The status should be failure
End
It "rejects negative days"
When call validate_input_python "stale" "days-before-stale" "-1"
The status should be failure
End
It "rejects non-numeric days"
When call validate_input_python "stale" "days-before-stale" "many"
The status should be failure
End
End
Context "when validating days-before-close input"
It "accepts valid day count"
When call validate_input_python "stale" "days-before-close" "7"
The status should be success
End
It "accepts minimum days"
When call validate_input_python "stale" "days-before-close" "1"
The status should be success
End
It "accepts reasonable maximum days"
When call validate_input_python "stale" "days-before-close" "365"
The status should be success
End
It "rejects zero days"
When call validate_input_python "stale" "days-before-close" "0"
The status should be failure
End
It "rejects negative days"
When call validate_input_python "stale" "days-before-close" "-1"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Stale"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "token"
The output should include "days-before-stale"
The output should include "days-before-close"
End
End
Context "when testing input requirements"
It "has all inputs as optional"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "" "all_optional"
The output should equal "none"
End
End
Context "when testing security validations"
It "validates against command injection in token"
When call validate_input_python "stale" "token" "ghp_token\`whoami\`"
The status should be failure
End
It "validates against variable expansion in days"
When call validate_input_python "stale" "days-before-stale" "30\${HOME}"
The status should be failure
End
It "validates against shell metacharacters in days"
When call validate_input_python "stale" "days-before-close" "7; rm -rf /"
The status should be failure
End
End
End

View File

@@ -0,0 +1,111 @@
#!/usr/bin/env shellspec
# Unit tests for sync-labels action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "sync-labels action"
ACTION_DIR="sync-labels"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts GitHub token expression"
When call uv run "_tests/shared/validation_core.py" --validate "sync-labels" "token" "\${{ github.token }}"
The status should be success
End
It "accepts classic GitHub token"
When call uv run "_tests/shared/validation_core.py" --validate "sync-labels" "token" "ghp_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "accepts fine-grained GitHub token"
When call uv run "_tests/shared/validation_core.py" --validate "sync-labels" "token" "github_pat_11ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "sync-labels" "token" "invalid-token"
The status should be failure
End
It "rejects token with command injection"
When call validate_input_python "sync-labels" "token" "ghp_token; rm -rf /"
The status should be failure
End
End
Context "when validating config-file input"
It "accepts valid config file"
When call uv run "_tests/shared/validation_core.py" --validate "sync-labels" "labels" ".github/labels.yml"
The status should be success
End
It "accepts config file with json extension"
When call uv run "_tests/shared/validation_core.py" --validate "sync-labels" "labels" ".github/labels.json"
The status should be success
End
It "rejects path traversal in config file"
When call validate_input_python "sync-labels" "labels" "../../../etc/passwd"
The status should be failure
End
It "rejects absolute path in config file"
When call validate_input_python "sync-labels" "labels" "/etc/passwd"
The status should be failure
End
It "rejects config file with command injection"
When call validate_input_python "sync-labels" "labels" "labels.yml; rm -rf /"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Sync labels"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "token"
The output should include "labels"
End
End
Context "when testing input requirements"
It "token input is optional"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "token" "optional"
The output should equal "optional"
End
It "labels input is required"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "labels" "required"
The output should equal "required"
End
End
Context "when testing security validations"
It "validates against path traversal in config file"
When call validate_input_python "sync-labels" "labels" "../../malicious.yml"
The status should be failure
End
It "validates against command injection in token"
When call validate_input_python "sync-labels" "token" "ghp_token\`whoami\`"
The status should be failure
End
It "validates against shell metacharacters in config file"
When call validate_input_python "sync-labels" "labels" "labels.yml && rm -rf /"
The status should be failure
End
End
End

View File

@@ -0,0 +1,156 @@
#!/usr/bin/env shellspec
# Unit tests for terraform-lint-fix action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "terraform-lint-fix action"
ACTION_DIR="terraform-lint-fix"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating token input"
It "accepts GitHub token expression"
When call validate_input_python "terraform-lint-fix" "token" "\${{ github.token }}"
The status should be success
End
It "accepts GitHub fine-grained token"
When call validate_input_python "terraform-lint-fix" "token" "ghp_abcdefghijklmnopqrstuvwxyz1234567890"
The status should be success
End
It "rejects invalid token format"
When call validate_input_python "terraform-lint-fix" "token" "invalid-token"
The status should be failure
End
It "rejects token with command injection"
When call validate_input_python "terraform-lint-fix" "token" "ghp_token; rm -rf /"
The status should be failure
End
It "accepts empty token (uses default)"
When call validate_input_python "terraform-lint-fix" "token" ""
The status should be success
End
End
Context "when validating terraform-version input"
It "accepts valid terraform version"
When call validate_input_python "terraform-lint-fix" "terraform-version" "1.5.0"
The status should be success
End
It "accepts latest terraform version"
When call validate_input_python "terraform-lint-fix" "terraform-version" "latest"
The status should be success
End
It "accepts terraform version with patch"
When call validate_input_python "terraform-lint-fix" "terraform-version" "1.5.7"
The status should be success
End
It "accepts terraform version with v prefix"
When call validate_input_python "terraform-lint-fix" "terraform-version" "v1.5.0"
The status should be success
End
It "rejects terraform version with command injection"
When call validate_input_python "terraform-lint-fix" "terraform-version" "1.5.0; rm -rf /"
The status should be failure
End
It "accepts empty terraform version (uses default)"
When call validate_input_python "terraform-lint-fix" "terraform-version" ""
The status should be success
End
End
Context "when validating working-directory input"
It "accepts current directory"
When call validate_input_python "terraform-lint-fix" "working-directory" "."
The status should be success
End
It "accepts relative directory"
When call validate_input_python "terraform-lint-fix" "working-directory" "terraform"
The status should be success
End
It "accepts nested directory"
When call validate_input_python "terraform-lint-fix" "working-directory" "infrastructure/terraform"
The status should be success
End
It "rejects path traversal"
When call validate_input_python "terraform-lint-fix" "working-directory" "../malicious"
The status should be failure
End
It "rejects absolute paths"
When call validate_input_python "terraform-lint-fix" "working-directory" "/etc/passwd"
The status should be failure
End
It "rejects directory with command injection"
When call validate_input_python "terraform-lint-fix" "working-directory" "terraform; rm -rf /"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Terraform Lint and Fix"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "token"
The output should include "terraform-version"
The output should include "working-directory"
End
End
Context "when testing input requirements"
It "has all inputs as optional"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "" "all_optional"
The output should equal "none"
End
End
Context "when testing security validations"
It "validates against path traversal in working directory"
When call validate_input_python "terraform-lint-fix" "working-directory" "../../malicious"
The status should be failure
End
It "validates against command injection in terraform version"
When call validate_input_python "terraform-lint-fix" "terraform-version" "1.5.0\`whoami\`"
The status should be failure
End
It "validates against shell metacharacters in token"
When call validate_input_python "terraform-lint-fix" "token" "ghp_token && rm -rf /"
The status should be failure
End
End
Context "when testing Terraform-specific validations"
It "validates terraform version format"
When call validate_input_python "terraform-lint-fix" "terraform-version" "1.x.x"
The status should be failure
End
It "validates working directory path safety"
When call validate_input_python "terraform-lint-fix" "working-directory" "/root/.ssh"
The status should be failure
End
End
End

View File

@@ -0,0 +1,178 @@
#!/usr/bin/env shellspec
# Unit tests for validate-inputs action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "validate-inputs action"
ACTION_DIR="validate-inputs"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating action input"
It "accepts valid action name"
When call validate_input_python "validate-inputs" "action" "github-release"
The status should be success
End
It "accepts action name with hyphens"
When call validate_input_python "validate-inputs" "action" "docker-build"
The status should be success
End
It "accepts action name with underscores"
When call validate_input_python "validate-inputs" "action" "npm_publish"
The status should be success
End
It "rejects action with command injection"
When call validate_input_python "validate-inputs" "action" "github-release; rm -rf /"
The status should be failure
End
It "rejects action with shell operators"
When call validate_input_python "validate-inputs" "action" "github-release && malicious"
The status should be failure
End
It "rejects action with pipe"
When call validate_input_python "validate-inputs" "action" "github-release | cat /etc/passwd"
The status should be failure
End
It "rejects empty action"
When call validate_input_python "validate-inputs" "action" ""
The status should be failure
End
End
Context "when validating rules-file input"
It "accepts valid rules file"
When call validate_input_python "validate-inputs" "rules-file" "validate-inputs/rules/github-release.yml"
The status should be success
End
It "accepts rules file with relative path"
When call validate_input_python "validate-inputs" "rules-file" "rules/action.yml"
The status should be success
End
It "rejects path traversal in rules file"
When call validate_input_python "validate-inputs" "rules-file" "../../../etc/passwd"
The status should be failure
End
It "rejects absolute path in rules file"
When call validate_input_python "validate-inputs" "rules-file" "/etc/passwd"
The status should be failure
End
It "rejects rules file with command injection"
When call validate_input_python "validate-inputs" "rules-file" "rules.yml; rm -rf /"
The status should be failure
End
It "accepts empty rules file (uses default)"
When call validate_input_python "validate-inputs" "rules-file" ""
The status should be success
End
End
Context "when validating fail-on-error input"
It "accepts true for fail-on-error"
When call validate_input_python "validate-inputs" "fail-on-error" "true"
The status should be success
End
It "accepts false for fail-on-error"
When call validate_input_python "validate-inputs" "fail-on-error" "false"
The status should be success
End
It "rejects invalid fail-on-error value"
When call validate_input_python "validate-inputs" "fail-on-error" "yes"
The status should be failure
End
It "rejects empty fail-on-error"
When call validate_input_python "validate-inputs" "fail-on-error" ""
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should equal "Validate Inputs"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "action"
The output should include "rules-file"
The output should include "fail-on-error"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "validation-result"
The output should include "errors-found"
The output should include "rules-applied"
End
End
Context "when testing input requirements"
It "requires action input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "action" "required"
The output should equal "required"
End
It "has rules-file as optional input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "rules-file" "optional"
The output should equal "optional"
End
It "has fail-on-error as optional input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "fail-on-error" "optional"
The output should equal "optional"
End
End
Context "when testing security validations"
It "validates against path traversal in rules file"
When call validate_input_python "validate-inputs" "rules-file" "../../malicious.yml"
The status should be failure
End
It "validates against command injection in action name"
When call validate_input_python "validate-inputs" "action" "test\`whoami\`"
The status should be failure
End
It "validates against shell metacharacters in rules file"
When call validate_input_python "validate-inputs" "rules-file" "rules.yml && rm -rf /"
The status should be failure
End
End
Context "when testing validation-specific functionality"
It "validates action name format restrictions"
When call validate_input_python "validate-inputs" "action" "invalid/action/name"
The status should be failure
End
It "validates rules file extension requirements"
When call validate_input_python "validate-inputs" "rules-file" "rules.txt"
The status should be success
End
It "validates boolean input parsing"
When call validate_input_python "validate-inputs" "fail-on-error" "TRUE"
The status should be success
End
End
End

View File

@@ -0,0 +1,125 @@
#!/usr/bin/env shellspec
# Unit tests for version-file-parser action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "version-file-parser action"
ACTION_DIR="version-file-parser"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating language input"
It "accepts valid language input"
When call validate_input_python "version-file-parser" "language" "node"
The status should be success
End
It "accepts php language"
When call validate_input_python "version-file-parser" "language" "php"
The status should be success
End
It "accepts python language"
When call validate_input_python "version-file-parser" "language" "python"
The status should be success
End
It "accepts go language"
When call validate_input_python "version-file-parser" "language" "go"
The status should be success
End
It "rejects invalid language with special characters"
When call validate_input_python "version-file-parser" "language" "node; rm -rf /"
The status should be failure
End
It "rejects empty required inputs"
When call validate_input_python "version-file-parser" "language" ""
The status should be failure
End
End
Context "when validating dockerfile-image input"
It "accepts valid dockerfile image"
When call validate_input_python "version-file-parser" "dockerfile-image" "node"
The status should be success
End
It "accepts php dockerfile image"
When call validate_input_python "version-file-parser" "dockerfile-image" "php"
The status should be success
End
It "accepts python dockerfile image"
When call validate_input_python "version-file-parser" "dockerfile-image" "python"
The status should be success
End
It "rejects injection in dockerfile image"
When call validate_input_python "version-file-parser" "dockerfile-image" "node;malicious"
The status should be failure
End
End
Context "when validating optional inputs"
It "accepts valid validation regex"
When call validate_input_python "version-file-parser" "validation-regex" "^[0-9]+\.[0-9]+(\.[0-9]+)?$"
The status should be success
End
It "accepts valid default version"
When call validate_input_python "version-file-parser" "default-version" "18.0.0"
The status should be success
End
It "accepts tool versions key"
When call validate_input_python "version-file-parser" "tool-versions-key" "nodejs"
The status should be success
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "contains required metadata"
When call get_action_name "$ACTION_FILE"
The output should equal "Version File Parser"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "language"
The output should include "tool-versions-key"
The output should include "dockerfile-image"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "detected-version"
The output should include "package-manager"
End
End
Context "when validating security"
It "rejects injection in language parameter"
When call validate_input_python "version-file-parser" "language" "node&&malicious"
The status should be failure
End
It "rejects pipe injection in tool versions key"
When call validate_input_python "version-file-parser" "tool-versions-key" "nodejs|dangerous"
The status should be failure
End
It "validates regex patterns safely"
When call validate_input_python "version-file-parser" "validation-regex" "^[0-9]+\.[0-9]+$"
The status should be success
End
It "rejects malicious regex patterns"
When call validate_input_python "version-file-parser" "validation-regex" ".*; rm -rf /"
The status should be failure
End
End
Context "when testing outputs"
It "produces all expected outputs consistently"
When call test_action_outputs "$ACTION_DIR" "language" "node" "dockerfile-image" "node"
The status should be success
The stderr should include "Testing action outputs for: version-file-parser"
The stderr should include "Output test passed for: version-file-parser"
End
End
End

View File

@@ -0,0 +1,233 @@
#!/usr/bin/env shellspec
# Unit tests for version-validator action validation and logic
# Framework is automatically loaded via spec_helper.sh
Describe "version-validator action"
ACTION_DIR="version-validator"
ACTION_FILE="$ACTION_DIR/action.yml"
Context "when validating version input"
It "accepts valid semantic version"
When call validate_input_python "version-validator" "version" "1.2.3"
The status should be success
End
It "accepts semantic version with v prefix"
When call validate_input_python "version-validator" "version" "v1.2.3"
The status should be success
End
It "accepts prerelease version"
When call validate_input_python "version-validator" "version" "1.2.3-alpha"
The status should be success
End
It "accepts prerelease with number"
When call validate_input_python "version-validator" "version" "1.2.3-alpha.1"
The status should be success
End
It "accepts build metadata"
When call validate_input_python "version-validator" "version" "1.2.3+build.1"
The status should be success
End
It "accepts prerelease with build metadata"
When call validate_input_python "version-validator" "version" "1.2.3-alpha.1+build.1"
The status should be success
End
It "accepts CalVer format"
When call validate_input_python "version-validator" "version" "2024.3.1"
The status should be success
End
It "rejects invalid version format"
When call validate_input_python "version-validator" "version" "invalid.version"
The status should be failure
End
It "rejects version with command injection"
When call validate_input_python "version-validator" "version" "1.2.3; rm -rf /"
The status should be failure
End
It "rejects version with shell expansion"
When call validate_input_python "version-validator" "version" "1.2.3\$(whoami)"
The status should be failure
End
It "rejects empty version"
When call validate_input_python "version-validator" "version" ""
The status should be failure
End
End
Context "when validating validation-regex input"
It "accepts valid regex pattern"
When call validate_input_python "version-validator" "validation-regex" "^[0-9]+\.[0-9]+\.[0-9]+$"
The status should be success
End
It "accepts semantic version regex"
When call validate_input_python "version-validator" "validation-regex" "^[0-9]+\.[0-9]+(\.[0-9]+)?(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$"
The status should be success
End
It "accepts empty validation-regex (uses default)"
When call validate_input_python "version-validator" "validation-regex" ""
The status should be success
End
It "accepts valid regex patterns with quantifiers"
When call validate_input_python "version-validator" "validation-regex" "^[0-9]+\\.[0-9]+$"
The status should be success
End
It "rejects regex with command injection"
When call validate_input_python "version-validator" "validation-regex" "^[0-9]+$; rm -rf /"
The status should be failure
End
End
Context "when validating ReDoS patterns"
It "rejects nested quantifiers (a+)+"
When call validate_input_python "version-validator" "validation-regex" "(a+)+"
The status should be failure
End
It "rejects nested quantifiers (a*)+"
When call validate_input_python "version-validator" "validation-regex" "(a*)+"
The status should be failure
End
It "rejects nested quantifiers (a+)*"
When call validate_input_python "version-validator" "validation-regex" "(a+)*"
The status should be failure
End
It "rejects nested quantifiers (a*)*"
When call validate_input_python "version-validator" "validation-regex" "(a*)*"
The status should be failure
End
It "rejects quantified groups (a+){2,5}"
When call validate_input_python "version-validator" "validation-regex" "(a+){2,5}"
The status should be failure
End
It "rejects consecutive quantifiers .*.* (ReDoS)"
When call validate_input_python "version-validator" "validation-regex" ".*.*"
The status should be failure
End
It "rejects consecutive quantifiers .*+ (ReDoS)"
When call validate_input_python "version-validator" "validation-regex" ".*+"
The status should be failure
End
It "rejects duplicate alternatives (a|a)+"
When call validate_input_python "version-validator" "validation-regex" "(a|a)+"
The status should be failure
End
It "rejects overlapping alternatives (a|ab)+"
When call validate_input_python "version-validator" "validation-regex" "(a|ab)+"
The status should be failure
End
It "accepts safe pattern with single quantifier"
When call validate_input_python "version-validator" "validation-regex" "^[0-9]+$"
The status should be success
End
It "accepts safe pattern with character class"
When call validate_input_python "version-validator" "validation-regex" "^[a-zA-Z0-9]+$"
The status should be success
End
It "accepts safe pattern with optional group"
When call validate_input_python "version-validator" "validation-regex" "^[0-9]+(\\.[0-9]+)?$"
The status should be success
End
It "accepts safe alternation without repetition"
When call validate_input_python "version-validator" "validation-regex" "^(alpha|beta|gamma)$"
The status should be success
End
End
Context "when validating language input"
It "accepts valid language name"
When call validate_input_python "version-validator" "language" "nodejs"
The status should be success
End
It "accepts version as language"
When call validate_input_python "version-validator" "language" "version"
The status should be success
End
It "accepts empty language (uses default)"
When call validate_input_python "version-validator" "language" ""
The status should be success
End
It "rejects language with command injection"
When call validate_input_python "version-validator" "language" "version; rm -rf /"
The status should be failure
End
End
Context "when checking action.yml structure"
It "has valid YAML syntax"
When call validate_action_yml_quiet "$ACTION_FILE"
The status should be success
End
It "has correct action name"
name=$(get_action_name "$ACTION_FILE")
When call echo "$name"
The output should match pattern "*Version*"
End
It "defines expected inputs"
When call get_action_inputs "$ACTION_FILE"
The output should include "version"
The output should include "validation-regex"
The output should include "language"
End
It "defines expected outputs"
When call get_action_outputs "$ACTION_FILE"
The output should include "is-valid"
The output should include "validated-version"
The output should include "error-message"
End
End
Context "when testing input requirements"
It "requires version input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "version" "required"
The status should be success
The output should equal "required"
End
It "has validation-regex as optional input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "validation-regex" "optional"
The status should be success
The output should equal "optional"
End
It "has language as optional input"
When call uv run "_tests/shared/validation_core.py" --property "$ACTION_FILE" "language" "optional"
The status should be success
The output should equal "optional"
End
End
Context "when testing security validations"
It "validates against path traversal in version"
When call validate_input_python "version-validator" "version" "../1.2.3"
The status should be failure
End
It "validates against shell metacharacters in version"
When call validate_input_python "version-validator" "version" "1.2.3|echo"
The status should be failure
End
It "validates against backtick injection in language"
When call validate_input_python "version-validator" "language" "version\`whoami\`"
The status should be failure
End
It "validates against variable expansion in version"
When call validate_input_python "version-validator" "version" "1.2.3\${HOME}"
The status should be failure
End
End
Context "when testing version validation functionality"
It "validates semantic version format restrictions"
When call validate_input_python "version-validator" "version" "1.2"
The status should be success
End
It "validates regex pattern safety"
When call validate_input_python "version-validator" "validation-regex" "^[0-9]+$"
The status should be success
End
It "validates language parameter format"
When call validate_input_python "version-validator" "language" "NODEJS"
The status should be success
End
It "validates complex version formats"
When call validate_input_python "version-validator" "version" "1.0.0-beta.1+exp.sha.5114f85"
The status should be success
End
End
End