145 lines
5.1 KiB
Go
145 lines
5.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io/fs"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/lixenwraith/logwisp/internal/plugin"
|
|
"github.com/lixenwraith/logwisp/internal/session"
|
|
"github.com/lixenwraith/logwisp/internal/testutil"
|
|
|
|
lconfig "github.com/lixenwraith/config"
|
|
)
|
|
|
|
func TestWatchErrorsDoNotTriggerReloadAndChangesAreCoalesced(t *testing.T) {
|
|
testLogger(t)
|
|
for _, event := range []string{lconfig.EventFileDeleted, lconfig.EventPermissionsChanged, lconfig.EventReloadTimeout, lconfig.EventReloadError + ":invalid TOML"} {
|
|
if collectConfigChanges(event, nil) {
|
|
t.Fatalf("error event triggered service restart: %s", event)
|
|
}
|
|
}
|
|
changes := make(chan string, 3)
|
|
changes <- "pipelines"
|
|
changes <- "status_reporter"
|
|
changes <- lconfig.EventReloadError + ":partial edit"
|
|
close(changes)
|
|
if !collectConfigChanges("logging.level", changes) || len(changes) != 0 {
|
|
t.Fatal("pending path changes were not folded into a single reload")
|
|
}
|
|
if collectConfigChanges("", changes) {
|
|
t.Fatal("closed channel triggered a reload")
|
|
}
|
|
}
|
|
|
|
func TestShippedConfigurationBuildsWithCheckedPluginDecoder(t *testing.T) {
|
|
testLogger(t)
|
|
path, err := filepath.Abs(filepath.Join("..", "..", "config", "logwisp.toml"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := loadTestConfig(t, path)
|
|
if len(cfg.Pipelines) != 1 || cfg.Pipelines[0].Name != "default" {
|
|
t.Fatalf("sample array-of-tables decoded incorrectly: %+v", cfg.Pipelines)
|
|
}
|
|
svc, err := bootstrapService(context.Background(), cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer svc.Shutdown()
|
|
pl, err := svc.GetPipeline("default")
|
|
if err != nil || len(pl.Sources) != 1 || len(pl.Sinks) != 1 {
|
|
t.Fatalf("sample plugins failed to build: %v", err)
|
|
}
|
|
svc.Shutdown() // A later reload failure or process exit can close it again.
|
|
}
|
|
|
|
func TestInvalidReloadLeavesStatusReporterRunning(t *testing.T) {
|
|
testLogger(t)
|
|
path := filepath.Join(t.TempDir(), "config.toml")
|
|
testutil.WriteFile(t, path, "[logging]\nlevel = \"info\"\n")
|
|
for _, mode := range []string{"empty-pipelines", "duplicate-names", "fractional-plugin-integer", "plugin-overflow"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
cfg := loadTestConfig(t, path)
|
|
switch mode {
|
|
case "empty-pipelines":
|
|
cfg.Pipelines = nil
|
|
case "duplicate-names":
|
|
cfg.Pipelines = append(cfg.Pipelines, cfg.Pipelines[0])
|
|
case "fractional-plugin-integer":
|
|
cfg.Pipelines[0].PluginSinks[0].Config["buffer_size"] = 1.5
|
|
case "plugin-overflow":
|
|
cfg.Pipelines[0].PluginSinks[0].Config["buffer_size"] = ^uint64(0)
|
|
}
|
|
cancelled := false
|
|
svc, next, _, err := handleReload(context.Background(), cfg, nil, func() { cancelled = true })
|
|
if err == nil || svc != nil || next != nil || cancelled {
|
|
t.Fatalf("invalid reload affected running state: svc=%v cfg=%v cancelled=%v error=%v", svc, next, cancelled, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Every registered plugin decodes through the checked decoder, so a typo in
|
|
// any plugin's config, security tables included, refuses to start: a typo in
|
|
// a listener's acl table, and an acl table on a plugin that listens on nothing.
|
|
func TestEveryPluginRejectsUnknownKeys(t *testing.T) {
|
|
testLogger(t)
|
|
manager := session.NewManager(time.Hour)
|
|
t.Cleanup(manager.Stop)
|
|
proxy := session.NewProxy(manager, "typo")
|
|
// the error names the key: acl.no_such_key on a listener, acl elsewhere
|
|
for key, bad := range map[string]map[string]any{
|
|
`"no_such_key"`: {"no_such_key": true},
|
|
`"acl`: {"acl": map[string]any{"no_such_key": true}},
|
|
} {
|
|
check := func(kind, name string, err error) {
|
|
if err == nil || !strings.Contains(err.Error(), "unknown key "+key) {
|
|
t.Errorf("%s %q accepted %v: %v", kind, name, bad, err)
|
|
}
|
|
}
|
|
for _, name := range plugin.ListSources() {
|
|
factory, _ := plugin.GetSource(name)
|
|
_, err := factory("typo", bad, logger, proxy)
|
|
check("source", name, err)
|
|
}
|
|
for _, name := range plugin.ListSinks() {
|
|
factory, _ := plugin.GetSink(name)
|
|
_, err := factory("typo", bad, logger, proxy)
|
|
check("sink", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// lw --check builds every plugin, so a bad option fails it, but starts none:
|
|
// the port a valid sink names stays free and its log directory uncreated.
|
|
func TestCheckBuildsWithoutStarting(t *testing.T) {
|
|
testLogger(t)
|
|
dir := t.TempDir()
|
|
path, out := filepath.Join(dir, "empty.toml"), filepath.Join(dir, "out")
|
|
testutil.WriteFile(t, path, "")
|
|
if code := checkConfig(loadTestConfig(t, path, "--source", "null", "--sink", "http,host=127.0.0.1,port=15862",
|
|
"--sink", "file,name=check,directory="+out)); code != 0 {
|
|
t.Fatalf("valid configuration: exit %d", code)
|
|
}
|
|
ln, err := net.Listen("tcp4", "127.0.0.1:15862")
|
|
if err != nil {
|
|
t.Fatalf("check bound the sink's port: %v", err)
|
|
}
|
|
ln.Close()
|
|
if _, err := os.Stat(out); !errors.Is(err, fs.ErrNotExist) {
|
|
t.Fatalf("check created the file sink's directory: %v", err)
|
|
}
|
|
missing := filepath.Join(dir, "missing")
|
|
if code := checkConfig(loadTestConfig(t, path, "--source", "null", "--sink",
|
|
"http,port=15862,tls.enabled=true,tls.cert_file="+missing+",tls.key_file="+missing)); code != 1 {
|
|
t.Fatalf("listener TLS without its certificate file: exit %d, want 1", code)
|
|
}
|
|
}
|