1// SPDX-License-Identifier: MIT
2
3package template
4
5import (
6 "strings"
7 "testing"
8)
9
10func readWorkflow(t testing.TB, src string) (Token, []string) {
11 t.Helper()
12 s, err := WorkflowSchema()
13 if err != nil {
14 t.Fatal(err)
15 }
16 ctx := NewContext(s)
17 tok := ReadYAML(ctx, "workflow-root-strict", "a.yml", []byte(src))
18 return tok, ctx.Messages()
19}
20
21const head = "on: push\njobs:\n a:\n runs-on: x\n steps:\n"
22
23func TestReadErrors(t *testing.T) {
24 // Expected messages come from @actions/workflow-parser 0.3.61 run on the
25 // same inputs, except that expression errors carry the position suffix of
26 // the C# runner (package expr follows it).
27 tests := []struct {
28 name, src string
29 want []string
30 }{
31 {"unknown context, block scalar", head + " - run: |\n echo hi\n echo ${{ foo.bar }}\n",
32 []string{"a.yml (Line: 6, Col: 14): Unrecognized named-value: 'foo'. Located at position 1 within expression: foo.bar"}},
33 {"unknown context, plain", head + " - run: echo ${{ foo.bar }} abc\n",
34 []string{"a.yml (Line: 6, Col: 14): Unrecognized named-value: 'foo'. Located at position 1 within expression: foo.bar"}},
35 {"unknown context, double quoted", head + " - run: \"echo ${{ foo.bar }} abc\"\n",
36 []string{"a.yml (Line: 6, Col: 14): Unrecognized named-value: 'foo'. Located at position 1 within expression: foo.bar"}},
37 {"bad expression and insert in a name", head + " - run: echo ${{ 1 + }}\n - name: ${{ insert }}\n run: x\n",
38 []string{"a.yml (Line: 6, Col: 14): Unexpected symbol: '+'. Located at position 3 within expression: 1 +", "a.yml (Line: 7, Col: 15): Unexpected value '${{ insert }}'"}},
39 {"not closed", head + " - run: echo ${{ 1\n",
40 []string{"a.yml (Line: 6, Col: 14): The expression is not closed. An unescaped ${{ sequence was found, but the closing }} sequence was not found."}},
41 {"empty document", "", []string{"a.yml: Expected a scalar value, a sequence, or a mapping"}},
42 {"comment only", "# c\n", []string{"a.yml: Expected a scalar value, a sequence, or a mapping"}},
43 {"sequence root", "[1,2]", []string{"a.yml (Line: 1, Col: 1): A sequence was not expected"}},
44 {"duplicate key", "on: push\non: pull_request\njobs: {}", []string{"a.yml (Line: 2, Col: 1): 'on' is already defined"}},
45 {"alias is resolved", "on: push\njobs:\n a: &x\n runs-on: x\n steps: [{run: a}]\n b: *x\n", nil},
46 {"valid", head + " - run: echo ${{ github.ref }}\n", nil},
47 }
48 for _, tc := range tests {
49 t.Run(tc.name, func(t *testing.T) {
50 _, got := readWorkflow(t, tc.src)
51 if strings.Join(got, "\n") != strings.Join(tc.want, "\n") {
52 t.Errorf("errors:\n got %q\nwant %q", got, tc.want)
53 }
54 })
55 }
56}
57
58func TestReadSyntaxErrorsHaveNoTokens(t *testing.T) {
59 for _, src := range []string{"on: [push\n", "a: b: c", "on: push\n---\nx: 1\n", "\t"} {
60 tok, errs := readWorkflow(t, src)
61 if tok != nil || len(errs) == 0 {
62 t.Errorf("%q: tok=%v errs=%q", src, tok, errs)
63 }
64 }
65}
66
67func TestMixedStringBecomesFormat(t *testing.T) {
68 tok, errs := readWorkflow(t, head+" - run: it's ${{ github.ref }} {x} ${{ 1 }}\n")
69 if len(errs) > 0 {
70 t.Fatal(errs)
71 }
72 run := tok.(*Mapping).Find("jobs").(*Mapping).Find("a").(*Mapping).Find("steps").(*Sequence).Items[0].(*Mapping).Find("run")
73 be, ok := run.(*BasicExpression)
74 if !ok {
75 t.Fatalf("run is %T", run)
76 }
77 if want := `format('it''s {0} {{x}} {1}', github.ref, 1)`; be.Expression != want {
78 t.Errorf("expression = %s, want %s", be.Expression, want)
79 }
80 if len(be.Original) != 2 {
81 t.Errorf("originals = %d", len(be.Original))
82 }
83}
84
85func TestEscapedLiteralExpression(t *testing.T) {
86 tok, errs := readWorkflow(t, head+" - run: ${{ '{{ literal }}' }}\n")
87 if len(errs) > 0 {
88 t.Fatal(errs)
89 }
90 run := tok.(*Mapping).Find("jobs").(*Mapping).Find("a").(*Mapping).Find("steps").(*Sequence).Items[0].(*Mapping).Find("run")
91 if s, ok := run.(*String); !ok || s.Value != "{{ literal }}" {
92 t.Errorf("run = %#v", run)
93 }
94}
95
96func TestScalarTypes(t *testing.T) {
97 tok, errs := readWorkflow(t, "on: push\njobs:\n a:\n runs-on: x\n timeout-minutes: 0x10\n continue-on-error: yes\n steps: [{run: 1}]\n")
98 // yes is a string in YAML 1.2, so continue-on-error is rejected.
99 if len(errs) == 0 || !strings.Contains(errs[0], "Unexpected value 'yes'") {
100 t.Errorf("errs = %q", errs)
101 }
102 job := tok.(*Mapping).Find("jobs").(*Mapping).Find("a").(*Mapping)
103 if n, ok := job.Find("timeout-minutes").(*Number); !ok || n.Value != 16 {
104 t.Errorf("timeout-minutes = %#v", job.Find("timeout-minutes"))
105 }
106 step := job.Find("steps").(*Sequence).Items[0].(*Mapping)
107 if s, ok := step.Find("run").(*String); !ok || s.Value != "1" {
108 t.Errorf("run = %#v", step.Find("run"))
109 }
110}
111
112func TestAliasBombIsRejected(t *testing.T) {
113 var b strings.Builder
114 b.WriteString("a0: &a0 [x, x, x, x, x, x, x, x, x, x]\n")
115 for i := 1; i < 8; i++ {
116 b.WriteString("a")
117 b.WriteString(string(rune('0' + i)))
118 b.WriteString(": &a")
119 b.WriteString(string(rune('0' + i)))
120 b.WriteString(" [")
121 for j := 0; j < 10; j++ {
122 b.WriteString("*a")
123 b.WriteString(string(rune('0' + i - 1)))
124 b.WriteString(",")
125 }
126 b.WriteString("]\n")
127 }
128 _, errs := readWorkflow(t, b.String())
129 if len(errs) == 0 || !strings.Contains(strings.Join(errs, "\n"), "too large") {
130 t.Errorf("errs = %q", errs)
131 }
132}
133
134func TestFileSizeLimit(t *testing.T) {
135 s, err := WorkflowSchema()
136 if err != nil {
137 t.Fatal(err)
138 }
139 big := []byte("on: push\n# " + strings.Repeat("x", MaxFileBytes) + "\n")
140 ctx := NewContext(s)
141 if tok := ReadYAML(ctx, "workflow-root-strict", "big.yml", big); tok != nil {
142 t.Error("token returned for an oversized file")
143 }
144 errs := ctx.Messages()
145 if len(errs) != 1 || !strings.HasPrefix(errs[0], "big.yml: The file is too large: ") {
146 t.Errorf("errors = %q", errs)
147 }
148 // Exactly at the limit is read.
149 ok := append([]byte("on: push\njobs: {}\n# "), make([]byte, 0)...)
150 pad := MaxFileBytes - len(ok) - 1
151 ok = append(ok, []byte(strings.Repeat("x", pad)+"\n")...)
152 if len(ok) != MaxFileBytes {
153 t.Fatalf("test file is %d bytes", len(ok))
154 }
155 ctx = NewContext(s)
156 ReadYAML(ctx, "workflow-root-strict", "ok.yml", ok)
157 for _, m := range ctx.Messages() {
158 if strings.Contains(m, "too large") {
159 t.Errorf("file at the limit refused: %s", m)
160 }
161 }
162}