mirror of https://github.com/caddyserver/caddy
533 lines
11 KiB
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{},
|
|
}
|
|
}
|