tenseleyflow/shithub / 10ddbae

Browse files

actions/expr: end-to-end fixture test for github.* alias (S41a-H1)

Reads tests/fixtures/workflows/github-alias.yml, parses the workflow,
extracts the ${{ … }} body from the run command, and evaluates it
through the alias path. The audit caught H1 because the fixture
existed but nothing exercised it end-to-end. Pin that gap closed.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
10ddbaea83854e8f2ad0ae011bf1a0c55aae536d
Parents
bc6092d
Tree
32018bb

1 changed file

StatusFile+-
M internal/actions/expr/eval_test.go 63 0
internal/actions/expr/eval_test.gomodified
@@ -3,10 +3,13 @@
33
 package expr_test
44
 
55
 import (
6
+	"os"
7
+	"path/filepath"
68
 	"strings"
79
 	"testing"
810
 
911
 	"github.com/tenseleyFlow/shithub/internal/actions/expr"
12
+	"github.com/tenseleyFlow/shithub/internal/actions/workflow"
1013
 )
1114
 
1215
 // evalString is the test helper that lex + parse + eval in one shot.
@@ -376,6 +379,66 @@ func TestEval_GithubUnknownFieldErrors(t *testing.T) {
376379
 	}
377380
 }
378381
 
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
+
379442
 func TestEval_JobStatusFunctions(t *testing.T) {
380443
 	t.Parallel()
381444
 	cases := []struct {