mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-11-04 03:27:23 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			206 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			206 lines
		
	
	
		
			5.7 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 (
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"net/http"
 | 
						|
	"reflect"
 | 
						|
	"sync"
 | 
						|
	"testing"
 | 
						|
)
 | 
						|
 | 
						|
var testCfg = []byte(`{
 | 
						|
			"apps": {
 | 
						|
				"http": {
 | 
						|
					"servers": {
 | 
						|
						"myserver": {
 | 
						|
							"listen": ["tcp/localhost:8080-8084"],
 | 
						|
							"read_timeout": "30s"
 | 
						|
						},
 | 
						|
						"yourserver": {
 | 
						|
							"listen": ["127.0.0.1:5000"],
 | 
						|
							"read_header_timeout": "15s"
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		`)
 | 
						|
 | 
						|
func TestUnsyncedConfigAccess(t *testing.T) {
 | 
						|
	// each test is performed in sequence, so
 | 
						|
	// each change builds on the previous ones;
 | 
						|
	// the config is not reset between tests
 | 
						|
	for i, tc := range []struct {
 | 
						|
		method    string
 | 
						|
		path      string // rawConfigKey will be prepended
 | 
						|
		payload   string
 | 
						|
		expect    string // JSON representation of what the whole config is expected to be after the request
 | 
						|
		shouldErr bool
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			method:  "POST",
 | 
						|
			path:    "",
 | 
						|
			payload: `{"foo": "bar", "list": ["a", "b", "c"]}`, // starting value
 | 
						|
			expect:  `{"foo": "bar", "list": ["a", "b", "c"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method:  "POST",
 | 
						|
			path:    "/foo",
 | 
						|
			payload: `"jet"`,
 | 
						|
			expect:  `{"foo": "jet", "list": ["a", "b", "c"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method:  "POST",
 | 
						|
			path:    "/bar",
 | 
						|
			payload: `{"aa": "bb", "qq": "zz"}`,
 | 
						|
			expect:  `{"foo": "jet", "bar": {"aa": "bb", "qq": "zz"}, "list": ["a", "b", "c"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method: "DELETE",
 | 
						|
			path:   "/bar/qq",
 | 
						|
			expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method:    "DELETE",
 | 
						|
			path:      "/bar/qq",
 | 
						|
			expect:    `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
 | 
						|
			shouldErr: true,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method:  "POST",
 | 
						|
			path:    "/list",
 | 
						|
			payload: `"e"`,
 | 
						|
			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method:  "PUT",
 | 
						|
			path:    "/list/3",
 | 
						|
			payload: `"d"`,
 | 
						|
			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method: "DELETE",
 | 
						|
			path:   "/list/3",
 | 
						|
			expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method:  "PATCH",
 | 
						|
			path:    "/list/3",
 | 
						|
			payload: `"d"`,
 | 
						|
			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d"]}`,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			method:  "POST",
 | 
						|
			path:    "/list/...",
 | 
						|
			payload: `["e", "f", "g"]`,
 | 
						|
			expect:  `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e", "f", "g"]}`,
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		err := unsyncedConfigAccess(tc.method, rawConfigKey+tc.path, []byte(tc.payload), nil)
 | 
						|
 | 
						|
		if tc.shouldErr && err == nil {
 | 
						|
			t.Fatalf("Test %d: Expected error return value, but got: %v", i, err)
 | 
						|
		}
 | 
						|
		if !tc.shouldErr && err != nil {
 | 
						|
			t.Fatalf("Test %d: Should not have had error return value, but got: %v", i, err)
 | 
						|
		}
 | 
						|
 | 
						|
		// decode the expected config so we can do a convenient DeepEqual
 | 
						|
		var expectedDecoded any
 | 
						|
		err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
 | 
						|
		if err != nil {
 | 
						|
			t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
 | 
						|
		}
 | 
						|
 | 
						|
		// make sure the resulting config is as we expect it
 | 
						|
		if !reflect.DeepEqual(rawCfg[rawConfigKey], expectedDecoded) {
 | 
						|
			t.Fatalf("Test %d:\nExpected:\n\t%#v\nActual:\n\t%#v",
 | 
						|
				i, expectedDecoded, rawCfg[rawConfigKey])
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// TestLoadConcurrent exercises Load under concurrent conditions
 | 
						|
// and is most useful under test with `-race` enabled.
 | 
						|
func TestLoadConcurrent(t *testing.T) {
 | 
						|
	var wg sync.WaitGroup
 | 
						|
 | 
						|
	for i := 0; i < 100; i++ {
 | 
						|
		wg.Add(1)
 | 
						|
		go func() {
 | 
						|
			_ = Load(testCfg, true)
 | 
						|
			wg.Done()
 | 
						|
		}()
 | 
						|
	}
 | 
						|
	wg.Wait()
 | 
						|
}
 | 
						|
 | 
						|
type fooModule struct {
 | 
						|
	IntField int
 | 
						|
	StrField string
 | 
						|
}
 | 
						|
 | 
						|
func (fooModule) CaddyModule() ModuleInfo {
 | 
						|
	return ModuleInfo{
 | 
						|
		ID:  "foo",
 | 
						|
		New: func() Module { return new(fooModule) },
 | 
						|
	}
 | 
						|
}
 | 
						|
func (fooModule) Start() error { return nil }
 | 
						|
func (fooModule) Stop() error  { return nil }
 | 
						|
 | 
						|
func TestETags(t *testing.T) {
 | 
						|
	RegisterModule(fooModule{})
 | 
						|
 | 
						|
	if err := Load([]byte(`{"admin": {"listen": "localhost:2999"}, "apps": {"foo": {"strField": "abc", "intField": 0}}}`), true); err != nil {
 | 
						|
		t.Fatalf("loading: %s", err)
 | 
						|
	}
 | 
						|
 | 
						|
	const key = "/" + rawConfigKey + "/apps/foo"
 | 
						|
 | 
						|
	// try update the config with the wrong etag
 | 
						|
	err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), fmt.Sprintf(`"/%s not_an_etag"`, rawConfigKey), false)
 | 
						|
	if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
 | 
						|
		t.Fatalf("expected precondition failed; got %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// get the etag
 | 
						|
	hash := etagHasher()
 | 
						|
	if err := readConfig(key, hash); err != nil {
 | 
						|
		t.Fatalf("reading: %s", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// do the same update with the correct key
 | 
						|
	err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), makeEtag(key, hash), false)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatalf("expected update to work; got %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// now try another update. The hash should no longer match and we should get precondition failed
 | 
						|
	err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 2}`), makeEtag(key, hash), false)
 | 
						|
	if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
 | 
						|
		t.Fatalf("expected precondition failed; got %v", err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func BenchmarkLoad(b *testing.B) {
 | 
						|
	for i := 0; i < b.N; i++ {
 | 
						|
		Load(testCfg, true)
 | 
						|
	}
 | 
						|
}
 |