mirror of
https://github.com/ivuorinen/ghaw-auditor.git
synced 2026-01-26 03:14:09 +00:00
673 lines
21 KiB
Python
673 lines
21 KiB
Python
"""Tests for parser module."""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from ghaw_auditor.models import ActionType, PermissionLevel
|
|
from ghaw_auditor.parser import Parser
|
|
|
|
FIXTURES_DIR = Path(__file__).parent / "fixtures"
|
|
|
|
|
|
def test_parser_initialization() -> None:
|
|
"""Test parser can be initialized."""
|
|
parser = Parser(Path.cwd())
|
|
assert parser.yaml is not None
|
|
|
|
|
|
# ============================================================================
|
|
# Workflow Parsing Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_parse_basic_workflow() -> None:
|
|
"""Test parsing a basic workflow."""
|
|
parser = Parser(FIXTURES_DIR)
|
|
workflow = parser.parse_workflow(FIXTURES_DIR / "basic-workflow.yml")
|
|
|
|
assert workflow.name == "Basic Workflow"
|
|
assert workflow.path == "basic-workflow.yml"
|
|
assert workflow.triggers == ["push"]
|
|
assert "test" in workflow.jobs
|
|
assert workflow.jobs["test"].runs_on == "ubuntu-latest"
|
|
assert len(workflow.jobs["test"].actions_used) == 1
|
|
assert workflow.jobs["test"].actions_used[0].owner == "actions"
|
|
assert workflow.jobs["test"].actions_used[0].repo == "checkout"
|
|
|
|
|
|
def test_parse_complex_workflow() -> None:
|
|
"""Test parsing a complex workflow with all features."""
|
|
parser = Parser(FIXTURES_DIR)
|
|
workflow = parser.parse_workflow(FIXTURES_DIR / "complex-workflow.yml")
|
|
|
|
# Basic metadata
|
|
assert workflow.name == "Complex Workflow"
|
|
assert set(workflow.triggers) == {"push", "pull_request", "workflow_dispatch"}
|
|
|
|
# Permissions
|
|
assert workflow.permissions is not None
|
|
assert workflow.permissions.contents == PermissionLevel.READ
|
|
assert workflow.permissions.issues == PermissionLevel.WRITE
|
|
assert workflow.permissions.pull_requests == PermissionLevel.WRITE
|
|
|
|
# Environment variables
|
|
assert workflow.env["NODE_ENV"] == "production"
|
|
assert workflow.env["API_URL"] == "https://api.example.com"
|
|
|
|
# Concurrency
|
|
assert workflow.concurrency is not None
|
|
|
|
# Defaults
|
|
assert workflow.defaults["run"]["shell"] == "bash"
|
|
|
|
# Jobs
|
|
assert "build" in workflow.jobs
|
|
assert "test" in workflow.jobs
|
|
|
|
# Build job
|
|
build = workflow.jobs["build"]
|
|
assert build.timeout_minutes == 30
|
|
assert build.permissions is not None
|
|
assert build.environment == {"name": "production", "url": "https://example.com"}
|
|
|
|
# Test job
|
|
test = workflow.jobs["test"]
|
|
assert test.needs == ["build"]
|
|
assert test.if_condition == "github.event_name == 'pull_request'"
|
|
assert test.container is not None
|
|
assert test.container.image == "node:20-alpine"
|
|
assert "NODE_ENV" in test.container.env
|
|
assert test.continue_on_error is True
|
|
|
|
# Services
|
|
assert "postgres" in test.services
|
|
assert test.services["postgres"].image == "postgres:15"
|
|
|
|
# Strategy
|
|
assert test.strategy is not None
|
|
assert test.strategy.fail_fast is False
|
|
assert test.strategy.max_parallel == 2
|
|
|
|
# Secrets extraction
|
|
assert "API_KEY" in workflow.secrets_used
|
|
assert "GITHUB_TOKEN" in workflow.secrets_used
|
|
assert "DATABASE_URL" in workflow.secrets_used
|
|
|
|
|
|
def test_parse_reusable_workflow() -> None:
|
|
"""Test parsing a reusable workflow."""
|
|
parser = Parser(FIXTURES_DIR)
|
|
workflow = parser.parse_workflow(FIXTURES_DIR / "reusable-workflow.yml")
|
|
|
|
assert workflow.is_reusable is True
|
|
assert workflow.reusable_contract is not None
|
|
|
|
# Check inputs
|
|
assert "environment" in workflow.reusable_contract.inputs
|
|
assert workflow.reusable_contract.inputs["environment"]["required"] is True
|
|
assert workflow.reusable_contract.inputs["debug"]["default"] is False
|
|
|
|
# Check outputs
|
|
assert "deployment-id" in workflow.reusable_contract.outputs
|
|
|
|
# Check secrets
|
|
assert "deploy-token" in workflow.reusable_contract.secrets
|
|
assert workflow.reusable_contract.secrets["deploy-token"]["required"] is True
|
|
|
|
|
|
def test_parse_workflow_with_empty_workflow_call() -> None:
|
|
"""Test parsing workflow with empty workflow_call."""
|
|
parser = Parser(FIXTURES_DIR)
|
|
workflow = parser.parse_workflow(FIXTURES_DIR / "empty-workflow-call.yml")
|
|
|
|
assert workflow.is_reusable is True
|
|
# Empty workflow_call should result in None contract
|
|
assert workflow.reusable_contract is None or workflow.reusable_contract.inputs == {}
|
|
|
|
|
|
def test_parse_empty_workflow() -> None:
|
|
"""Test parsing an empty workflow file raises error."""
|
|
parser = Parser(FIXTURES_DIR)
|
|
|
|
with pytest.raises(ValueError, match="Empty workflow file"):
|
|
parser.parse_workflow(FIXTURES_DIR / "invalid-workflow.yml")
|
|
|
|
|
|
# ============================================================================
|
|
# Action Reference Parsing Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_parse_action_ref_github() -> None:
|
|
"""Test parsing GitHub action reference."""
|
|
parser = Parser(Path.cwd())
|
|
ref = parser._parse_action_ref("actions/checkout@v4", Path("test.yml"))
|
|
|
|
assert ref.type == ActionType.GITHUB
|
|
assert ref.owner == "actions"
|
|
assert ref.repo == "checkout"
|
|
assert ref.ref == "v4"
|
|
|
|
|
|
def test_parse_action_ref_github_with_path() -> None:
|
|
"""Test parsing GitHub action reference with path (monorepo)."""
|
|
parser = Parser(Path.cwd())
|
|
ref = parser._parse_action_ref("owner/repo/path/to/action@v1", Path("test.yml"))
|
|
|
|
assert ref.type == ActionType.GITHUB
|
|
assert ref.owner == "owner"
|
|
assert ref.repo == "repo"
|
|
assert ref.path == "path/to/action"
|
|
assert ref.ref == "v1"
|
|
|
|
|
|
def test_parse_action_ref_local() -> None:
|
|
"""Test parsing local action reference."""
|
|
parser = Parser(Path.cwd())
|
|
ref = parser._parse_action_ref("./.github/actions/custom", Path("test.yml"))
|
|
|
|
assert ref.type == ActionType.LOCAL
|
|
assert ref.path == "./.github/actions/custom"
|
|
|
|
|
|
def test_parse_action_ref_docker() -> None:
|
|
"""Test parsing Docker action reference."""
|
|
parser = Parser(Path.cwd())
|
|
ref = parser._parse_action_ref("docker://alpine:3.8", Path("test.yml"))
|
|
|
|
assert ref.type == ActionType.DOCKER
|
|
assert ref.path == "docker://alpine:3.8"
|
|
|
|
|
|
def test_parse_action_ref_invalid() -> None:
|
|
"""Test parsing invalid action reference raises error."""
|
|
parser = Parser(Path.cwd())
|
|
|
|
with pytest.raises(ValueError, match="Invalid action reference"):
|
|
parser._parse_action_ref("invalid-ref", Path("test.yml"))
|
|
|
|
|
|
def test_extract_secrets() -> None:
|
|
"""Test extracting secrets from content."""
|
|
parser = Parser(Path.cwd())
|
|
content = """
|
|
env:
|
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
API_KEY: ${{ secrets.API_KEY }}
|
|
"""
|
|
secrets = parser._extract_secrets(content)
|
|
|
|
assert "GITHUB_TOKEN" in secrets
|
|
assert "API_KEY" in secrets
|
|
assert len(secrets) == 2
|
|
|
|
|
|
# ============================================================================
|
|
# Trigger Extraction Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_extract_triggers_string() -> None:
|
|
"""Test extracting triggers from string."""
|
|
parser = Parser(Path.cwd())
|
|
triggers = parser._extract_triggers("push")
|
|
|
|
assert triggers == ["push"]
|
|
|
|
|
|
def test_extract_triggers_list() -> None:
|
|
"""Test extracting triggers from list."""
|
|
parser = Parser(Path.cwd())
|
|
triggers = parser._extract_triggers(["push", "pull_request"])
|
|
|
|
assert triggers == ["push", "pull_request"]
|
|
|
|
|
|
def test_extract_triggers_dict() -> None:
|
|
"""Test extracting triggers from dict."""
|
|
parser = Parser(Path.cwd())
|
|
triggers = parser._extract_triggers(
|
|
{
|
|
"push": {"branches": ["main"]},
|
|
"pull_request": None,
|
|
"workflow_dispatch": None,
|
|
}
|
|
)
|
|
|
|
assert set(triggers) == {"push", "pull_request", "workflow_dispatch"}
|
|
|
|
|
|
def test_extract_triggers_empty() -> None:
|
|
"""Test extracting triggers from empty value."""
|
|
parser = Parser(Path.cwd())
|
|
triggers = parser._extract_triggers(None)
|
|
|
|
assert triggers == []
|
|
|
|
|
|
# ============================================================================
|
|
# Permissions Parsing Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_parse_permissions_none() -> None:
|
|
"""Test parsing None permissions."""
|
|
parser = Parser(Path.cwd())
|
|
perms = parser._parse_permissions(None)
|
|
|
|
assert perms is None
|
|
|
|
|
|
def test_parse_permissions_string() -> None:
|
|
"""Test parsing string permissions (read-all/write-all)."""
|
|
parser = Parser(Path.cwd())
|
|
perms = parser._parse_permissions("read-all")
|
|
|
|
# Should return an empty Permissions object
|
|
assert perms is not None
|
|
|
|
|
|
def test_parse_permissions_dict() -> None:
|
|
"""Test parsing dict permissions."""
|
|
parser = Parser(Path.cwd())
|
|
perms = parser._parse_permissions(
|
|
{
|
|
"contents": "read",
|
|
"issues": "write",
|
|
"pull_requests": "write",
|
|
}
|
|
)
|
|
|
|
assert perms is not None
|
|
assert perms.contents == PermissionLevel.READ
|
|
assert perms.issues == PermissionLevel.WRITE
|
|
assert perms.pull_requests == PermissionLevel.WRITE
|
|
|
|
|
|
# ============================================================================
|
|
# Job Parsing Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_parse_job_with_none_data() -> None:
|
|
"""Test parsing job with None data."""
|
|
parser = Parser(Path.cwd())
|
|
job = parser._parse_job("test", None, Path("test.yml"), "")
|
|
|
|
assert job.name == "test"
|
|
assert job.runs_on == "ubuntu-latest" # default value
|
|
|
|
|
|
def test_parse_job_needs_string_vs_list() -> None:
|
|
"""Test parsing job needs as string vs list."""
|
|
parser = Parser(Path.cwd())
|
|
|
|
# String needs
|
|
job1 = parser._parse_job("test", {"needs": "build"}, Path("test.yml"), "")
|
|
assert job1.needs == ["build"]
|
|
|
|
# List needs
|
|
job2 = parser._parse_job("test", {"needs": ["build", "lint"]}, Path("test.yml"), "")
|
|
assert job2.needs == ["build", "lint"]
|
|
|
|
|
|
def test_parse_job_with_none_steps() -> None:
|
|
"""Test parsing job with None steps."""
|
|
parser = Parser(Path.cwd())
|
|
job = parser._parse_job(
|
|
"test",
|
|
{"steps": [None, {"uses": "actions/checkout@v4"}]},
|
|
Path("test.yml"),
|
|
"",
|
|
)
|
|
|
|
# Should skip None steps
|
|
assert len(job.actions_used) == 1
|
|
assert job.actions_used[0].repo == "checkout"
|
|
|
|
|
|
# ============================================================================
|
|
# Container/Services/Strategy Parsing Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_parse_container_none() -> None:
|
|
"""Test parsing None container."""
|
|
parser = Parser(Path.cwd())
|
|
container = parser._parse_container(None)
|
|
|
|
assert container is None
|
|
|
|
|
|
def test_parse_container_string() -> None:
|
|
"""Test parsing container from string."""
|
|
parser = Parser(Path.cwd())
|
|
container = parser._parse_container("ubuntu:latest")
|
|
|
|
assert container is not None
|
|
assert container.image == "ubuntu:latest"
|
|
|
|
|
|
def test_parse_container_dict() -> None:
|
|
"""Test parsing container from dict."""
|
|
parser = Parser(Path.cwd())
|
|
container = parser._parse_container(
|
|
{
|
|
"image": "node:20",
|
|
"credentials": {"username": "user", "password": "pass"},
|
|
"env": {"NODE_ENV": "test"},
|
|
"ports": [8080],
|
|
"volumes": ["/tmp:/tmp"],
|
|
"options": "--cpus 2",
|
|
}
|
|
)
|
|
|
|
assert container is not None
|
|
assert container.image == "node:20"
|
|
assert container.credentials == {"username": "user", "password": "pass"}
|
|
assert container.env["NODE_ENV"] == "test"
|
|
assert container.ports == [8080]
|
|
assert container.volumes == ["/tmp:/tmp"]
|
|
assert container.options == "--cpus 2"
|
|
|
|
|
|
def test_parse_services_none() -> None:
|
|
"""Test parsing None services."""
|
|
parser = Parser(Path.cwd())
|
|
services = parser._parse_services(None)
|
|
|
|
assert services == {}
|
|
|
|
|
|
def test_parse_services_string_image() -> None:
|
|
"""Test parsing service with string image."""
|
|
parser = Parser(Path.cwd())
|
|
services = parser._parse_services({"postgres": "postgres:15"})
|
|
|
|
assert "postgres" in services
|
|
assert services["postgres"].name == "postgres"
|
|
assert services["postgres"].image == "postgres:15"
|
|
|
|
|
|
def test_parse_services_dict() -> None:
|
|
"""Test parsing service with dict config."""
|
|
parser = Parser(Path.cwd())
|
|
services = parser._parse_services(
|
|
{
|
|
"redis": {
|
|
"image": "redis:alpine",
|
|
"ports": [6379],
|
|
"options": "--health-cmd 'redis-cli ping'",
|
|
}
|
|
}
|
|
)
|
|
|
|
assert "redis" in services
|
|
assert services["redis"].image == "redis:alpine"
|
|
assert services["redis"].ports == [6379]
|
|
|
|
|
|
def test_parse_strategy_none() -> None:
|
|
"""Test parsing None strategy."""
|
|
parser = Parser(Path.cwd())
|
|
strategy = parser._parse_strategy(None)
|
|
|
|
assert strategy is None
|
|
|
|
|
|
def test_parse_strategy_matrix() -> None:
|
|
"""Test parsing strategy with matrix."""
|
|
parser = Parser(Path.cwd())
|
|
strategy = parser._parse_strategy(
|
|
{
|
|
"matrix": {"node-version": [18, 20], "os": ["ubuntu-latest", "windows-latest"]},
|
|
"fail-fast": False,
|
|
"max-parallel": 4,
|
|
}
|
|
)
|
|
|
|
assert strategy is not None
|
|
assert strategy.matrix == {"node-version": [18, 20], "os": ["ubuntu-latest", "windows-latest"]}
|
|
assert strategy.fail_fast is False
|
|
assert strategy.max_parallel == 4
|
|
|
|
|
|
# ============================================================================
|
|
# Action Manifest Parsing Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_parse_composite_action() -> None:
|
|
"""Test parsing a composite action."""
|
|
parser = Parser(FIXTURES_DIR)
|
|
action = parser.parse_action(FIXTURES_DIR / "composite-action.yml")
|
|
|
|
assert action.name == "Composite Action"
|
|
assert action.description == "A composite action example"
|
|
assert action.author == "Test Author"
|
|
assert action.is_composite is True
|
|
assert action.is_docker is False
|
|
assert action.is_javascript is False
|
|
|
|
# Check inputs
|
|
assert "message" in action.inputs
|
|
assert action.inputs["message"].required is True
|
|
assert "debug" in action.inputs
|
|
assert action.inputs["debug"].required is False
|
|
assert action.inputs["debug"].default == "false"
|
|
|
|
# Check outputs
|
|
assert "result" in action.outputs
|
|
assert action.outputs["result"].description == "Action result"
|
|
|
|
# Check branding
|
|
assert action.branding is not None
|
|
|
|
|
|
def test_parse_docker_action() -> None:
|
|
"""Test parsing a Docker action."""
|
|
parser = Parser(FIXTURES_DIR)
|
|
action = parser.parse_action(FIXTURES_DIR / "docker-action.yml")
|
|
|
|
assert action.name == "Docker Action"
|
|
assert action.is_docker is True
|
|
assert action.is_composite is False
|
|
assert action.is_javascript is False
|
|
|
|
# Check inputs
|
|
assert "dockerfile" in action.inputs
|
|
assert action.inputs["dockerfile"].default == "Dockerfile"
|
|
|
|
# Check outputs
|
|
assert "image-id" in action.outputs
|
|
|
|
|
|
def test_parse_javascript_action() -> None:
|
|
"""Test parsing a JavaScript action."""
|
|
parser = Parser(FIXTURES_DIR)
|
|
action = parser.parse_action(FIXTURES_DIR / "javascript-action.yml")
|
|
|
|
assert action.name == "JavaScript Action"
|
|
assert action.is_javascript is True
|
|
assert action.is_composite is False
|
|
assert action.is_docker is False
|
|
|
|
# Check runs config
|
|
assert action.runs["using"] == "node20"
|
|
assert action.runs["main"] == "dist/index.js"
|
|
|
|
|
|
def test_parse_action_with_various_defaults() -> None:
|
|
"""Test parsing action with different input default types."""
|
|
parser = Parser(FIXTURES_DIR)
|
|
action = parser.parse_action(FIXTURES_DIR / "action-with-defaults.yml")
|
|
|
|
assert action.name == "Action with Various Defaults"
|
|
|
|
# String default
|
|
assert action.inputs["string-input"].default == "hello"
|
|
|
|
# Boolean default
|
|
assert action.inputs["boolean-input"].default is True
|
|
|
|
# Number default
|
|
assert action.inputs["number-input"].default == 42
|
|
|
|
# No default
|
|
assert action.inputs["no-default"].required is True
|
|
|
|
|
|
def test_parse_action_empty_inputs_outputs() -> None:
|
|
"""Test parsing action with empty inputs/outputs."""
|
|
parser = Parser(FIXTURES_DIR)
|
|
action = parser.parse_action(FIXTURES_DIR / "composite-action.yml")
|
|
|
|
# Even if action has inputs/outputs, the parser should handle missing ones
|
|
assert action.inputs is not None
|
|
assert action.outputs is not None
|
|
|
|
|
|
def test_parse_empty_action() -> None:
|
|
"""Test parsing an empty action file raises error."""
|
|
parser = Parser(FIXTURES_DIR)
|
|
|
|
with pytest.raises(ValueError, match="Empty action file"):
|
|
parser.parse_action(FIXTURES_DIR / "invalid-action.yml")
|
|
|
|
|
|
# ============================================================================
|
|
# Reusable Workflow Tests
|
|
# ============================================================================
|
|
|
|
|
|
def test_parse_reusable_workflow_caller() -> None:
|
|
"""Test parsing workflow that calls reusable workflows."""
|
|
parser = Parser(FIXTURES_DIR)
|
|
workflow = parser.parse_workflow(FIXTURES_DIR / "reusable-workflow-caller.yml")
|
|
|
|
assert workflow.name == "Reusable Workflow Caller"
|
|
assert "call-workflow" in workflow.jobs
|
|
assert "call-workflow-inherit" in workflow.jobs
|
|
assert "call-local-workflow" in workflow.jobs
|
|
|
|
# Test job with explicit secrets
|
|
call_job = workflow.jobs["call-workflow"]
|
|
assert call_job.uses == "owner/repo/.github/workflows/deploy.yml@v1"
|
|
assert call_job.with_inputs["environment"] == "production"
|
|
assert call_job.with_inputs["debug"] is False
|
|
assert call_job.with_inputs["version"] == "1.2.3"
|
|
assert call_job.secrets_passed is not None
|
|
assert "deploy-token" in call_job.secrets_passed
|
|
assert call_job.inherit_secrets is False
|
|
|
|
# Verify reusable workflow tracked as action
|
|
assert len(call_job.actions_used) == 1
|
|
assert call_job.actions_used[0].type == ActionType.REUSABLE_WORKFLOW
|
|
assert call_job.actions_used[0].owner == "owner"
|
|
assert call_job.actions_used[0].repo == "repo"
|
|
assert call_job.actions_used[0].path == ".github/workflows/deploy.yml"
|
|
assert call_job.actions_used[0].ref == "v1"
|
|
|
|
# Test job with inherited secrets
|
|
inherit_job = workflow.jobs["call-workflow-inherit"]
|
|
assert inherit_job.uses == "owner/repo/.github/workflows/test.yml@main"
|
|
assert inherit_job.inherit_secrets is True
|
|
assert inherit_job.secrets_passed is None
|
|
|
|
# Test local reusable workflow
|
|
local_job = workflow.jobs["call-local-workflow"]
|
|
assert local_job.uses == "./.github/workflows/shared.yml"
|
|
assert local_job.actions_used[0].type == ActionType.REUSABLE_WORKFLOW
|
|
assert local_job.actions_used[0].path == "./.github/workflows/shared.yml"
|
|
|
|
|
|
def test_parse_job_with_outputs() -> None:
|
|
"""Test parsing job with outputs."""
|
|
parser = Parser(FIXTURES_DIR)
|
|
workflow = parser.parse_workflow(FIXTURES_DIR / "job-with-outputs.yml")
|
|
|
|
assert "build" in workflow.jobs
|
|
build_job = workflow.jobs["build"]
|
|
|
|
assert build_job.outputs is not None
|
|
assert "version" in build_job.outputs
|
|
assert "artifact-url" in build_job.outputs
|
|
assert "status" in build_job.outputs
|
|
assert build_job.outputs["status"] == "success"
|
|
|
|
|
|
def test_parse_reusable_workflow_ref_local() -> None:
|
|
"""Test parsing local reusable workflow reference."""
|
|
parser = Parser(Path.cwd())
|
|
ref = parser._parse_reusable_workflow_ref("./.github/workflows/deploy.yml", Path("test.yml"))
|
|
|
|
assert ref.type == ActionType.REUSABLE_WORKFLOW
|
|
assert ref.path == "./.github/workflows/deploy.yml"
|
|
|
|
|
|
def test_parse_reusable_workflow_ref_github() -> None:
|
|
"""Test parsing GitHub reusable workflow reference."""
|
|
parser = Parser(Path.cwd())
|
|
ref = parser._parse_reusable_workflow_ref("actions/reusable/.github/workflows/build.yml@v1", Path("test.yml"))
|
|
|
|
assert ref.type == ActionType.REUSABLE_WORKFLOW
|
|
assert ref.owner == "actions"
|
|
assert ref.repo == "reusable"
|
|
assert ref.path == ".github/workflows/build.yml"
|
|
assert ref.ref == "v1"
|
|
|
|
|
|
def test_parse_reusable_workflow_ref_invalid() -> None:
|
|
"""Test parsing invalid reusable workflow reference raises error."""
|
|
parser = Parser(Path.cwd())
|
|
|
|
with pytest.raises(ValueError, match="Invalid reusable workflow reference"):
|
|
parser._parse_reusable_workflow_ref("invalid-workflow-ref", Path("test.yml"))
|
|
|
|
|
|
def test_parse_permissions_invalid_type(tmp_path: Path) -> None:
|
|
"""Test parsing permissions with invalid type."""
|
|
parser = Parser(tmp_path)
|
|
|
|
# Test with boolean (invalid type)
|
|
result = parser._parse_permissions(True)
|
|
assert result is None
|
|
|
|
# Test with int (invalid type)
|
|
result = parser._parse_permissions(123)
|
|
assert result is None
|
|
|
|
# Test with list (invalid type)
|
|
result = parser._parse_permissions(["read", "write"])
|
|
assert result is None
|
|
|
|
|
|
def test_parse_workflow_with_boolean_and_number_env(tmp_path: Path) -> None:
|
|
"""Test parsing workflow with boolean and number values in env."""
|
|
workflow_file = tmp_path / "test.yml"
|
|
workflow_file.write_text(
|
|
"""
|
|
name: Test
|
|
on: push
|
|
env:
|
|
STRING_VAR: "hello"
|
|
BOOL_VAR: true
|
|
NUMBER_VAR: 42
|
|
FLOAT_VAR: 3.14
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo test
|
|
"""
|
|
)
|
|
|
|
parser = Parser(tmp_path)
|
|
workflow = parser.parse_workflow(workflow_file)
|
|
|
|
assert workflow.env["STRING_VAR"] == "hello"
|
|
assert workflow.env["BOOL_VAR"] is True
|
|
assert workflow.env["NUMBER_VAR"] == 42
|
|
assert workflow.env["FLOAT_VAR"] == 3.14
|