@@ -3,10 +3,13 @@ |
| 3 | 3 | package expr_test |
| 4 | 4 | |
| 5 | 5 | import ( |
| 6 | + "os" |
| 7 | + "path/filepath" |
| 6 | 8 | "strings" |
| 7 | 9 | "testing" |
| 8 | 10 | |
| 9 | 11 | "github.com/tenseleyFlow/shithub/internal/actions/expr" |
| 12 | + "github.com/tenseleyFlow/shithub/internal/actions/workflow" |
| 10 | 13 | ) |
| 11 | 14 | |
| 12 | 15 | // evalString is the test helper that lex + parse + eval in one shot. |
@@ -376,6 +379,66 @@ func TestEval_GithubUnknownFieldErrors(t *testing.T) { |
| 376 | 379 | } |
| 377 | 380 | } |
| 378 | 381 | |
| 382 | +// TestEval_GithubAliasFixtureEndToEnd lifts the actual github-alias.yml |
| 383 | +// fixture, parses the workflow, extracts the `${{ … }}` body from the |
| 384 | +// step's run command, and evaluates it through the alias path. This is |
| 385 | +// the end-to-end pin that would have caught S41a-H1: the fixture |
| 386 | +// existed but no test exercised it through eval. |
| 387 | +func TestEval_GithubAliasFixtureEndToEnd(t *testing.T) { |
| 388 | + t.Parallel() |
| 389 | + src, err := os.ReadFile(filepath.Join("../../../tests/fixtures/workflows", "github-alias.yml")) |
| 390 | + if err != nil { |
| 391 | + t.Fatalf("read fixture: %v", err) |
| 392 | + } |
| 393 | + w, diags, err := workflow.Parse(src) |
| 394 | + if err != nil { |
| 395 | + t.Fatalf("parse fixture: %v", err) |
| 396 | + } |
| 397 | + if len(diags) != 0 { |
| 398 | + t.Fatalf("unexpected diagnostics: %v", diags) |
| 399 | + } |
| 400 | + // Step 0's run command is: echo "${{ github.event.head_commit.message }}" |
| 401 | + run := w.Jobs[0].Steps[0].Run |
| 402 | + body, ok := extractFirstExpression(run) |
| 403 | + if !ok { |
| 404 | + t.Fatalf("expected ${{ ... }} expression in run command, got %q", run) |
| 405 | + } |
| 406 | + v, err := evalString(t, body, &expr.Context{ |
| 407 | + Shithub: expr.ShithubContext{ |
| 408 | + Event: map[string]any{ |
| 409 | + "head_commit": map[string]any{ |
| 410 | + "message": "WIP: from fixture", |
| 411 | + }, |
| 412 | + }, |
| 413 | + }, |
| 414 | + Untrusted: expr.DefaultUntrusted(), |
| 415 | + }) |
| 416 | + if err != nil { |
| 417 | + t.Fatalf("eval %q: %v", body, err) |
| 418 | + } |
| 419 | + if v.S != "WIP: from fixture" { |
| 420 | + t.Errorf("got %q, want %q", v.S, "WIP: from fixture") |
| 421 | + } |
| 422 | + if !v.Tainted { |
| 423 | + t.Error("github.event.head_commit.message must be tainted (S41d injection guard)") |
| 424 | + } |
| 425 | +} |
| 426 | + |
| 427 | +// extractFirstExpression pulls the body of the first `${{ … }}` block |
| 428 | +// out of s. Tiny helper used by the fixture round-trip test; the real |
| 429 | +// runner-side templating in S41d will be more sophisticated. |
| 430 | +func extractFirstExpression(s string) (string, bool) { |
| 431 | + start := strings.Index(s, "${{") |
| 432 | + if start < 0 { |
| 433 | + return "", false |
| 434 | + } |
| 435 | + end := strings.Index(s[start:], "}}") |
| 436 | + if end < 0 { |
| 437 | + return "", false |
| 438 | + } |
| 439 | + return strings.TrimSpace(s[start+3 : start+end]), true |
| 440 | +} |
| 441 | + |
| 379 | 442 | func TestEval_JobStatusFunctions(t *testing.T) { |
| 380 | 443 | t.Parallel() |
| 381 | 444 | cases := []struct { |