11package mutate
22
33import (
4+ "strings"
45 "testing"
56
67 "github.com/stretchr/testify/assert"
@@ -11,6 +12,8 @@ import (
1112 routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
1213 hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
1314 "google.golang.org/protobuf/types/known/anypb"
15+
16+ "go.datum.net/network-services-operator/internal/extensionserver/assets"
1417)
1518
1619const (
@@ -218,3 +221,84 @@ func TestInjectLocalReplyConfig_NoOp(t *testing.T) {
218221 })
219222 }
220223}
224+
225+ func TestEscapeEnvoyFormatLiterals (t * testing.T ) {
226+ tests := []struct {
227+ name string
228+ in string
229+ want string
230+ }{
231+ {"empty" , "" , "" },
232+ {"no percent" , "<html>plain</html>" , "<html>plain</html>" },
233+ {"bare percent in css" , "height: 100%;" , "height: 100%%;" },
234+ {"multiple bare percents" , "120% 50% 0%" , "120%% 50%% 0%%" },
235+ {"preserve allowlisted command" , "code %RESPONSE_CODE% end" , "code %RESPONSE_CODE% end" },
236+ {"escape non-allowlisted command" , "%REQ(x-header)%" , "%%REQ(x-header)%%" },
237+ {"escape non-allowlisted command with length" , "%REQ(x-header):10%" , "%%REQ(x-header):10%%" },
238+ {"escape command-shaped literal" , "progress %COMPLETE% now" , "progress %%COMPLETE%% now" },
239+ {"already escaped stays escaped" , "width: 100%%;" , "width: 100%%;" },
240+ {"mixed command and literal" , "%RESPONSE_CODE% at 100%" , "%RESPONSE_CODE% at 100%%" },
241+ {"trailing bare percent" , "50%" , "50%%" },
242+ }
243+ for _ , tc := range tests {
244+ t .Run (tc .name , func (t * testing.T ) {
245+ got := escapeEnvoyFormatLiterals (tc .in )
246+ assert .Equal (t , tc .want , got )
247+ assert .Equal (t , got , escapeEnvoyFormatLiterals (got ), "must be idempotent" )
248+ })
249+ }
250+ }
251+
252+ // TestEmbeddedDefaultPageIsEnvoySafe is the regression guard for issue #243:
253+ // the embedded default error page contains literal CSS percent signs
254+ // (e.g. "height: 100%") that Envoy would misparse as command operators and
255+ // reject the listener. After escaping, no bare '%' may remain and the intended
256+ // %RESPONSE_CODE% operator must survive.
257+ func TestEmbeddedDefaultPageIsEnvoySafe (t * testing.T ) {
258+ raw := assets .DefaultError5xxHTML
259+ require .Contains (t , raw , "height: 100%;" , "test premise: raw page has an unescaped percent" )
260+
261+ escaped := escapeEnvoyFormatLiterals (raw )
262+
263+ assert .Contains (t , escaped , "height: 100%%;" , "literal percent must be escaped" )
264+ assert .Equal (t , 1 , strings .Count (escaped , "%RESPONSE_CODE%" ), "command operator must be preserved exactly once" )
265+ assert .Equal (t , escaped , escapeEnvoyFormatLiterals (escaped ), "escaping must be idempotent" )
266+
267+ // The escaped body must satisfy the same invariant the startup validator
268+ // asserts: no bare '%' Envoy could misparse as a command operator.
269+ assert .NoError (t , assertEnvoyFormatSafe (escaped ), "no bare percent may remain after escaping" )
270+ }
271+
272+ // TestValidateLocalReplyConfig covers the startup guard (issue #243): the
273+ // assembled config must validate for the embedded default, reject a body whose
274+ // bare '%' would reach Envoy unescaped, and no-op when injection is disabled.
275+ func TestValidateLocalReplyConfig (t * testing.T ) {
276+ base := testLocalReplyConfig ()
277+
278+ t .Run ("embedded default is valid" , func (t * testing.T ) {
279+ cfg := * base
280+ cfg .BodyHTML = assets .DefaultError5xxHTML
281+ assert .NoError (t , ValidateLocalReplyConfig (& cfg ))
282+ })
283+
284+ t .Run ("escaped body is valid" , func (t * testing.T ) {
285+ cfg := * base
286+ cfg .BodyHTML = "height: 100%;"
287+ assert .NoError (t , ValidateLocalReplyConfig (& cfg ))
288+ })
289+
290+ t .Run ("disabled is a no-op" , func (t * testing.T ) {
291+ cfg := * base
292+ cfg .Disabled = true
293+ cfg .BodyHTML = "height: 100%;"
294+ assert .NoError (t , ValidateLocalReplyConfig (& cfg ))
295+ })
296+
297+ t .Run ("bare percent surviving into the body is rejected" , func (t * testing.T ) {
298+ // buildLocalReplyConfig escapes, so this asserts the raw invariant the
299+ // validator enforces on the injected body.
300+ assert .Error (t , assertEnvoyFormatSafe ("height: 100%;" ))
301+ assert .Error (t , assertEnvoyFormatSafe ("%COMPLETE%" ))
302+ assert .NoError (t , assertEnvoyFormatSafe ("code %RESPONSE_CODE% ok" ))
303+ })
304+ }
0 commit comments