mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 03:23:59 +00:00
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:
675
_tests/README.md
Normal file
675
_tests/README.md
Normal 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
239
_tests/framework/setup.sh
Executable 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
352
_tests/framework/utils.sh
Executable 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
885
_tests/framework/validation.py
Executable 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()
|
||||
241
_tests/integration/workflows/version-file-parser-test.yml
Normal file
241
_tests/integration/workflows/version-file-parser-test.yml
Normal 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
757
_tests/run-tests.sh
Executable 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
|
||||
62
_tests/shared/test_docker_image_regex.py
Executable file
62
_tests/shared/test_docker_image_regex.py
Executable 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
882
_tests/shared/validation_core.py
Executable 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()
|
||||
150
_tests/unit/ansible-lint-fix/validation.spec.sh
Executable file
150
_tests/unit/ansible-lint-fix/validation.spec.sh
Executable 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
|
||||
149
_tests/unit/biome-check/validation.spec.sh
Executable file
149
_tests/unit/biome-check/validation.spec.sh
Executable 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
|
||||
148
_tests/unit/biome-fix/validation.spec.sh
Executable file
148
_tests/unit/biome-fix/validation.spec.sh
Executable 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
|
||||
377
_tests/unit/codeql-analysis/validation.spec.sh
Executable file
377
_tests/unit/codeql-analysis/validation.spec.sh
Executable 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
|
||||
168
_tests/unit/common-cache/validation.spec.sh
Executable file
168
_tests/unit/common-cache/validation.spec.sh
Executable 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
|
||||
99
_tests/unit/common-file-check/validation.spec.sh
Executable file
99
_tests/unit/common-file-check/validation.spec.sh
Executable 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
|
||||
165
_tests/unit/common-retry/validation.spec.sh
Executable file
165
_tests/unit/common-retry/validation.spec.sh
Executable 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
|
||||
52
_tests/unit/compress-images/validation.spec.sh
Executable file
52
_tests/unit/compress-images/validation.spec.sh
Executable 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
|
||||
81
_tests/unit/csharp-build/validation.spec.sh
Executable file
81
_tests/unit/csharp-build/validation.spec.sh
Executable 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
|
||||
36
_tests/unit/csharp-lint-check/validation.spec.sh
Executable file
36
_tests/unit/csharp-lint-check/validation.spec.sh
Executable 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
|
||||
52
_tests/unit/csharp-publish/validation.spec.sh
Executable file
52
_tests/unit/csharp-publish/validation.spec.sh
Executable 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
|
||||
218
_tests/unit/docker-build/validation.spec.sh
Executable file
218
_tests/unit/docker-build/validation.spec.sh
Executable 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
|
||||
40
_tests/unit/docker-publish-gh/validation.spec.sh
Executable file
40
_tests/unit/docker-publish-gh/validation.spec.sh
Executable 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
|
||||
48
_tests/unit/docker-publish-hub/validation.spec.sh
Executable file
48
_tests/unit/docker-publish-hub/validation.spec.sh
Executable 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
|
||||
48
_tests/unit/docker-publish/validation.spec.sh
Executable file
48
_tests/unit/docker-publish/validation.spec.sh
Executable 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
|
||||
85
_tests/unit/dotnet-version-detect/validation.spec.sh
Executable file
85
_tests/unit/dotnet-version-detect/validation.spec.sh
Executable 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
|
||||
355
_tests/unit/eslint-check/validation.spec.sh
Executable file
355
_tests/unit/eslint-check/validation.spec.sh
Executable 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
|
||||
115
_tests/unit/eslint-fix/validation.spec.sh
Executable file
115
_tests/unit/eslint-fix/validation.spec.sh
Executable 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
|
||||
141
_tests/unit/github-release/validation.spec.sh
Executable file
141
_tests/unit/github-release/validation.spec.sh
Executable 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
|
||||
173
_tests/unit/go-build/validation.spec.sh
Executable file
173
_tests/unit/go-build/validation.spec.sh
Executable 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
|
||||
255
_tests/unit/go-lint/validation.spec.sh
Executable file
255
_tests/unit/go-lint/validation.spec.sh
Executable 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
|
||||
171
_tests/unit/go-version-detect/validation.spec.sh
Executable file
171
_tests/unit/go-version-detect/validation.spec.sh
Executable 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
|
||||
242
_tests/unit/node-setup/validation.spec.sh
Executable file
242
_tests/unit/node-setup/validation.spec.sh
Executable 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
|
||||
216
_tests/unit/npm-publish/validation.spec.sh
Executable file
216
_tests/unit/npm-publish/validation.spec.sh
Executable 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
|
||||
407
_tests/unit/php-composer/validation.spec.sh
Executable file
407
_tests/unit/php-composer/validation.spec.sh
Executable 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
|
||||
280
_tests/unit/php-laravel-phpunit/validation.spec.sh
Executable file
280
_tests/unit/php-laravel-phpunit/validation.spec.sh
Executable 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
|
||||
249
_tests/unit/php-tests/validation.spec.sh
Executable file
249
_tests/unit/php-tests/validation.spec.sh
Executable 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
|
||||
170
_tests/unit/php-version-detect/validation.spec.sh
Executable file
170
_tests/unit/php-version-detect/validation.spec.sh
Executable 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
|
||||
90
_tests/unit/pr-lint/validation.spec.sh
Executable file
90
_tests/unit/pr-lint/validation.spec.sh
Executable 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
|
||||
172
_tests/unit/pre-commit/validation.spec.sh
Executable file
172
_tests/unit/pre-commit/validation.spec.sh
Executable 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
|
||||
332
_tests/unit/prettier-check/validation.spec.sh
Executable file
332
_tests/unit/prettier-check/validation.spec.sh
Executable 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
|
||||
285
_tests/unit/prettier-fix/validation.spec.sh
Executable file
285
_tests/unit/prettier-fix/validation.spec.sh
Executable 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
|
||||
149
_tests/unit/python-lint-fix/validation.spec.sh
Executable file
149
_tests/unit/python-lint-fix/validation.spec.sh
Executable 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
|
||||
98
_tests/unit/python-version-detect-v2/validation.spec.sh
Executable file
98
_tests/unit/python-version-detect-v2/validation.spec.sh
Executable 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
|
||||
108
_tests/unit/python-version-detect/validation.spec.sh
Executable file
108
_tests/unit/python-version-detect/validation.spec.sh
Executable 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
|
||||
125
_tests/unit/release-monthly/validation.spec.sh
Executable file
125
_tests/unit/release-monthly/validation.spec.sh
Executable 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
|
||||
69
_tests/unit/set-git-config/validation.spec.sh
Executable file
69
_tests/unit/set-git-config/validation.spec.sh
Executable 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
579
_tests/unit/spec_helper.sh
Executable 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
|
||||
139
_tests/unit/stale/validation.spec.sh
Executable file
139
_tests/unit/stale/validation.spec.sh
Executable 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
|
||||
111
_tests/unit/sync-labels/validation.spec.sh
Executable file
111
_tests/unit/sync-labels/validation.spec.sh
Executable 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
|
||||
156
_tests/unit/terraform-lint-fix/validation.spec.sh
Executable file
156
_tests/unit/terraform-lint-fix/validation.spec.sh
Executable 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
|
||||
178
_tests/unit/validate-inputs/validation.spec.sh
Executable file
178
_tests/unit/validate-inputs/validation.spec.sh
Executable 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
|
||||
125
_tests/unit/version-file-parser/validation.spec.sh
Executable file
125
_tests/unit/version-file-parser/validation.spec.sh
Executable 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
|
||||
233
_tests/unit/version-validator/validation.spec.sh
Executable file
233
_tests/unit/version-validator/validation.spec.sh
Executable 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
|
||||
Reference in New Issue
Block a user