gx 0.8.0

CLI to manage Github Actions dependencies
Documentation
#![expect(
    clippy::unwrap_used,
    clippy::indexing_slicing,
    reason = "tests use unwrap and indexing freely"
)]

use super::*;

fn parse(content: &str) -> Parsed {
    Parsed::from_yaml(WorkflowPath::new(".github/workflows/x.yml"), content).unwrap()
}

#[test]
fn parses_bare_string_trigger() {
    let p = parse("on: push\njobs: {}\n");
    assert_eq!(p.on, vec![Trigger::Push]);
}

#[test]
fn parses_list_triggers() {
    let p = parse("on: [push, pull_request]\njobs: {}\n");
    assert_eq!(p.on, vec![Trigger::Push, Trigger::PullRequest]);
}

#[test]
fn parses_map_triggers_with_filters() {
    let p = parse(
        "on:
  push:
    branches: [main]
  pull_request_target:
    types: [labeled]
jobs: {}
",
    );
    assert!(p.has_trigger(&Trigger::Push));
    assert!(p.has_trigger(&Trigger::PullRequestTarget));
}

#[test]
fn unknown_trigger_round_trips_as_other() {
    let p = parse("on: deployment_status\njobs: {}\n");
    assert_eq!(p.on, vec![Trigger::Other("deployment_status".to_owned())]);
}

#[test]
fn permissions_read_all_shorthand() {
    let p = parse("on: push\npermissions: read-all\njobs: {}\n");
    assert_eq!(p.permissions, Some(Permissions::ReadAll));
    assert!(p.permissions.as_ref().unwrap().is_excessive());
}

#[test]
fn permissions_write_all_is_excessive_and_writable() {
    let p = parse("on: push\npermissions: write-all\njobs: {}\n");
    assert_eq!(p.permissions, Some(Permissions::WriteAll));
    assert!(p.permissions.as_ref().unwrap().is_excessive());
    assert!(p.permissions.as_ref().unwrap().has_write());
}

#[test]
fn permissions_contents_read_only_is_not_excessive() {
    let p = parse("on: push\npermissions:\n  contents: read\njobs: {}\n");
    let perms = p.permissions.unwrap();
    assert!(!perms.is_excessive());
    assert!(!perms.has_write());
}

#[test]
fn permissions_with_packages_write_is_excessive() {
    let p = parse(
        "on: push
permissions:
  contents: read
  packages: write
jobs: {}
",
    );
    let perms = p.permissions.unwrap();
    assert!(perms.is_excessive());
    assert!(perms.has_write());
}

#[test]
fn empty_permissions_map_drops_defaults() {
    let p = parse("on: push\npermissions: {}\njobs: {}\n");
    assert_eq!(p.permissions, Some(Permissions::Empty));
    assert!(!p.permissions.as_ref().unwrap().is_excessive());
}

#[test]
fn concurrency_captures_group_and_cancel() {
    let p = parse(
        "on: push
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true
jobs: {}
",
    );
    let c = p.concurrency.unwrap();
    assert_eq!(c.group.as_deref(), Some("ci-${{ github.ref }}"));
    assert_eq!(c.cancel_in_progress, Some(true));
}

#[test]
fn jobs_populate_id_from_map_key() {
    let p = parse(
        "on: push
jobs:
  build:
    steps:
      - uses: actions/checkout@v4
  test:
    steps: []
",
    );
    let mut ids: Vec<&str> = p.jobs.iter().map(|j| j.id.as_str()).collect();
    ids.sort_unstable();
    assert_eq!(ids, vec!["build", "test"]);
}

#[test]
fn step_captures_with_env_run_and_if() {
    let p = parse(
        "on: pull_request
jobs:
  build:
    steps:
      - uses: docker/login-action@v3
        if: github.event.pull_request.head.repo.full_name == github.repository
        with:
          username: foo
          password: ${{ secrets.DOCKER_HUB_TOKEN }}
        env:
          NODE_ENV: production
      - run: echo hello
",
    );
    let job = &p.jobs[0];
    let s0 = &job.steps[0];
    assert_eq!(s0.uses.as_deref(), Some("docker/login-action@v3"));
    assert!(
        s0.if_cond
            .as_deref()
            .unwrap()
            .contains("head.repo.full_name")
    );
    assert_eq!(s0.with.get("ref"), None);
    assert!(
        s0.with["password"]
            .as_str()
            .contains("secrets.DOCKER_HUB_TOKEN")
    );
    assert_eq!(s0.env["NODE_ENV"].as_str(), "production");
    let s1 = &job.steps[1];
    assert_eq!(s1.run.as_deref(), Some("echo hello"));
}

#[test]
fn scalar_text_concatenates_with_env_run() {
    let p = parse(
        "on: pull_request
jobs:
  build:
    steps:
      - with:
          password: ${{ secrets.MY_TOKEN }}
        env:
          OTHER: ${{ secrets.OTHER_TOKEN }}
        run: echo done
",
    );
    let text = p.jobs[0].steps[0].scalar_text();
    assert!(text.contains("secrets.MY_TOKEN"));
    assert!(text.contains("secrets.OTHER_TOKEN"));
    assert!(text.contains("echo done"));
}

#[test]
fn job_secrets_inherit_is_distinguished_from_explicit() {
    let p = parse(
        "on: workflow_call
jobs:
  call:
    uses: ./.github/workflows/x.yml
    secrets: inherit
",
    );
    assert_eq!(p.jobs[0].secrets, Some(JobSecrets::Inherit));
}

#[test]
fn any_scalar_accepts_numbers_and_bools() {
    let p = parse(
        "on: push
jobs:
  build:
    steps:
      - with:
          retries: 3
          verbose: true
",
    );
    let s = &p.jobs[0].steps[0];
    assert_eq!(s.with["retries"].as_str(), "3");
    assert_eq!(s.with["verbose"].as_str(), "true");
}