caddy/replacer_test.go

533 lines
11 KiB
Go

// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddy
import (
"fmt"
"os"
"path/filepath"
"runtime"
"sync"
"testing"
)
func TestReplacer(t *testing.T) {
type testCase struct {
input, expect, empty string
}
rep := testReplacer()
// ReplaceAll
for i, tc := range []testCase{
{
input: "{",
expect: "{",
},
{
input: `\{`,
expect: `{`,
},
{
input: "foo{",
expect: "foo{",
},
{
input: `foo\{`,
expect: `foo{`,
},
{
input: "foo{bar",
expect: "foo{bar",
},
{
input: `foo\{bar`,
expect: `foo{bar`,
},
{
input: "foo{bar}",
expect: "foo",
},
{
input: `foo\{bar\}`,
expect: `foo{bar}`,
},
{
input: "}",
expect: "}",
},
{
input: `\}`,
expect: `}`,
},
{
input: "{}",
expect: "",
},
{
input: `\{\}`,
expect: `{}`,
},
{
input: `{"json": "object"}`,
expect: "",
},
{
input: `\{"json": "object"}`,
expect: `{"json": "object"}`,
},
{
input: `\{"json": "object"\}`,
expect: `{"json": "object"}`,
},
{
input: `\{"json": "object{bar}"\}`,
expect: `{"json": "object"}`,
},
{
input: `\{"json": \{"nested": "object"\}\}`,
expect: `{"json": {"nested": "object"}}`,
},
{
input: `\{"json": \{"nested": "{bar}"\}\}`,
expect: `{"json": {"nested": ""}}`,
},
{
input: `pre \{"json": \{"nested": "{bar}"\}\}`,
expect: `pre {"json": {"nested": ""}}`,
},
{
input: `\{"json": \{"nested": "{bar}"\}\} post`,
expect: `{"json": {"nested": ""}} post`,
},
{
input: `pre \{"json": \{"nested": "{bar}"\}\} post`,
expect: `pre {"json": {"nested": ""}} post`,
},
{
input: `{{`,
expect: "{{",
},
{
input: `{{}`,
expect: "",
},
{
input: `{"json": "object"\}`,
expect: "",
},
{
input: `{unknown}`,
empty: "-",
expect: "-",
},
{
input: `back\slashes`,
expect: `back\slashes`,
},
{
input: `double back\\slashes`,
expect: `double back\\slashes`,
},
{
input: `placeholder {with \{ brace} in name`,
expect: `placeholder in name`,
},
{
input: `placeholder {with \} brace} in name`,
expect: `placeholder in name`,
},
{
input: `placeholder {with \} \} braces} in name`,
expect: `placeholder in name`,
},
{
input: `\{'group':'default','max_age':3600,'endpoints':[\{'url':'https://some.domain.local/a/d/g'\}],'include_subdomains':true\}`,
expect: `{'group':'default','max_age':3600,'endpoints':[{'url':'https://some.domain.local/a/d/g'}],'include_subdomains':true}`,
},
{
input: `{}{}{}{\\\\}\\\\`,
expect: `{\\\}\\\\`,
},
{
input: string([]byte{0x26, 0x00, 0x83, 0x7B, 0x84, 0x07, 0x5C, 0x7D, 0x84}),
expect: string([]byte{0x26, 0x00, 0x83, 0x7B, 0x84, 0x07, 0x7D, 0x84}),
},
{
input: `\\}`,
expect: `\}`,
},
} {
actual := rep.ReplaceAll(tc.input, tc.empty)
if actual != tc.expect {
t.Errorf("Test %d: '%s': expected '%s' but got '%s'",
i, tc.input, tc.expect, actual)
}
}
}
func TestReplacerSet(t *testing.T) {
rep := testReplacer()
for _, tc := range []struct {
variable string
value any
}{
{
variable: "test1",
value: "val1",
},
{
variable: "asdf",
value: "123",
},
{
variable: "numbers",
value: 123.456,
},
{
variable: "äöü",
value: "öö_äü",
},
{
variable: "with space",
value: "space value",
},
{
variable: "1",
value: "test-123",
},
{
variable: "mySuper_IP",
value: "1.2.3.4",
},
{
variable: "testEmpty",
value: "",
},
} {
rep.Set(tc.variable, tc.value)
// test if key is added
if val, ok := rep.static[tc.variable]; ok {
if val != tc.value {
t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
}
} else {
t.Errorf("Expected existing key '%s' found nothing", tc.variable)
}
}
// test if all keys are still there (by length)
length := len(rep.static)
if len(rep.static) != 8 {
t.Errorf("Expected length '%v' got '%v'", 7, length)
}
}
func TestReplacerReplaceKnown(t *testing.T) {
rep := Replacer{
mapMutex: &sync.RWMutex{},
providers: []replacementProvider{
// split our possible vars to two functions (to test if both functions are called)
ReplacerFunc(func(key string) (val any, ok bool) {
switch key {
case "test1":
return "val1", true
case "asdf":
return "123", true
case "äöü":
return "öö_äü", true
case "with space":
return "space value", true
default:
return "NOOO", false
}
}),
ReplacerFunc(func(key string) (val any, ok bool) {
switch key {
case "1":
return "test-123", true
case "mySuper_IP":
return "1.2.3.4", true
case "testEmpty":
return "", true
default:
return "NOOO", false
}
}),
},
}
for _, tc := range []struct {
testInput string
expected string
}{
{
// test vars without space
testInput: "{test1}{asdf}{äöü}{1}{with space}{mySuper_IP}",
expected: "val1123öö_äütest-123space value1.2.3.4",
},
{
// test vars with space
testInput: "{test1} {asdf} {äöü} {1} {with space} {mySuper_IP} ",
expected: "val1 123 öö_äü test-123 space value 1.2.3.4 ",
},
{
// test with empty val
testInput: "{test1} {testEmpty} {asdf} {1} ",
expected: "val1 EMPTY 123 test-123 ",
},
{
// test vars with not finished placeholders
testInput: "{te{test1}{as{{df{1}",
expected: "{teval1{as{{dftest-123",
},
{
// test with non existing vars
testInput: "{test1} {nope} {1} ",
expected: "val1 {nope} test-123 ",
},
} {
actual := rep.ReplaceKnown(tc.testInput, "EMPTY")
// test if all are replaced as expected
if actual != tc.expected {
t.Errorf("Expected '%s' got '%s' for '%s'", tc.expected, actual, tc.testInput)
}
}
}
func TestReplacerDelete(t *testing.T) {
rep := Replacer{
mapMutex: &sync.RWMutex{},
static: map[string]any{
"key1": "val1",
"key2": "val2",
"key3": "val3",
"key4": "val4",
},
}
startLen := len(rep.static)
toDel := []string{
"key2", "key4",
}
for _, key := range toDel {
rep.Delete(key)
// test if key is removed from static map
if _, ok := rep.static[key]; ok {
t.Errorf("Expected '%s' to be removed. It is still in static map.", key)
}
}
// check if static slice is smaller
expected := startLen - len(toDel)
actual := len(rep.static)
if len(rep.static) != expected {
t.Errorf("Expected length '%v' got length '%v'", expected, actual)
}
}
func TestReplacerMap(t *testing.T) {
rep := testReplacer()
for i, tc := range []ReplacerFunc{
func(key string) (val any, ok bool) {
return "", false
},
func(key string) (val any, ok bool) {
return "", false
},
} {
rep.Map(tc)
// test if function (which listens on specific key) is added by checking length
if len(rep.providers) == i+1 {
// check if the last function is the one we just added
pTc := fmt.Sprintf("%p", tc)
pRep := fmt.Sprintf("%p", rep.providers[i])
if pRep != pTc {
t.Errorf("Expected func pointer '%s' got '%s'", pTc, pRep)
}
} else {
t.Errorf("Expected providers length '%v' got length '%v'", i+1, len(rep.providers))
}
}
}
func TestReplacerNew(t *testing.T) {
repl := NewReplacer()
if len(repl.providers) != 3 {
t.Errorf("Expected providers length '%v' got length '%v'", 3, len(repl.providers))
}
// test if default global replacements are added as the first provider
hostname, _ := os.Hostname()
wd, _ := os.Getwd()
os.Setenv("CADDY_REPLACER_TEST", "envtest")
defer os.Setenv("CADDY_REPLACER_TEST", "")
for _, tc := range []struct {
variable string
value string
}{
{
variable: "system.hostname",
value: hostname,
},
{
variable: "system.slash",
value: string(filepath.Separator),
},
{
variable: "system.os",
value: runtime.GOOS,
},
{
variable: "system.arch",
value: runtime.GOARCH,
},
{
variable: "system.wd",
value: wd,
},
{
variable: "env.CADDY_REPLACER_TEST",
value: "envtest",
},
} {
if val, ok := repl.providers[0].replace(tc.variable); ok {
if val != tc.value {
t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
}
} else {
t.Errorf("Expected key '%s' to be recognized by first provider", tc.variable)
}
}
// test if file provider is added as the second provider
for _, tc := range []struct {
variable string
value string
}{
{
variable: "file.caddytest/integration/testdata/foo.txt",
value: "foo",
},
{
variable: "file.caddytest/integration/testdata/foo_with_trailing_newline.txt",
value: "foo",
},
{
variable: "file.caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt",
value: "foo" + getEOL(),
},
} {
if val, ok := repl.providers[1].replace(tc.variable); ok {
if val != tc.value {
t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
}
} else {
t.Errorf("Expected key '%s' to be recognized by second provider", tc.variable)
}
}
}
func getEOL() string {
if os.PathSeparator == '\\' {
return "\r\n" // Windows EOL
}
return "\n" // Unix and modern macOS EOL
}
func TestReplacerNewWithoutFile(t *testing.T) {
repl := NewReplacer().WithoutFile()
for _, tc := range []struct {
variable string
value string
notFound bool
}{
{
variable: "file.caddytest/integration/testdata/foo.txt",
notFound: true,
},
{
variable: "system.os",
value: runtime.GOOS,
},
} {
if val, ok := repl.Get(tc.variable); ok && !tc.notFound {
if val != tc.value {
t.Errorf("Expected value '%s' for key '%s' got '%s'", tc.value, tc.variable, val)
}
} else if !tc.notFound {
t.Errorf("Expected key '%s' to be recognized", tc.variable)
}
}
}
func BenchmarkReplacer(b *testing.B) {
type testCase struct {
name, input, empty string
}
rep := testReplacer()
rep.Set("str", "a string")
rep.Set("int", 123.456)
for _, bm := range []testCase{
{
name: "no placeholder",
input: `simple string`,
},
{
name: "string replacement",
input: `str={str}`,
},
{
name: "int replacement",
input: `int={int}`,
},
{
name: "placeholder",
input: `{"json": "object"}`,
},
{
name: "escaped placeholder",
input: `\{"json": \{"nested": "{bar}"\}\}`,
},
} {
b.Run(bm.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
rep.ReplaceAll(bm.input, bm.empty)
}
})
}
}
func testReplacer() Replacer {
return Replacer{
providers: make([]replacementProvider, 0),
static: make(map[string]any),
mapMutex: &sync.RWMutex{},
}
}