171 lines
5.4 KiB
Go
171 lines
5.4 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
|
||
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"flag"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"sourcedock.dev/petrbalvin/interpres/v2"
|
||
|
|
)
|
||
|
|
|
||
|
|
const sampleConfig = `listen = ":2049"
|
||
|
|
log-ops = true
|
||
|
|
state-dir = "/var/lib/nfsd"
|
||
|
|
max-connections = 64
|
||
|
|
|
||
|
|
[tls]
|
||
|
|
cert = "/etc/nfsd/cert.pem"
|
||
|
|
key = "/etc/nfsd/key.pem"
|
||
|
|
|
||
|
|
[[export]]
|
||
|
|
path = "/srv/demo"
|
||
|
|
read-only = true
|
||
|
|
`
|
||
|
|
|
||
|
|
func writeConfig(t *testing.T, body string) string {
|
||
|
|
t.Helper()
|
||
|
|
p := filepath.Join(t.TempDir(), "nfsd.toml")
|
||
|
|
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
|
||
|
|
t.Fatalf("WriteFile: %v", err)
|
||
|
|
}
|
||
|
|
return p
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLoadConfig reads the whole sample back, every table and key.
|
||
|
|
func TestLoadConfig(t *testing.T) {
|
||
|
|
cfg, err := loadConfig(writeConfig(t, sampleConfig))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("load: %v", err)
|
||
|
|
}
|
||
|
|
if cfg.Listen != ":2049" || !cfg.LogOps || cfg.StateDir != "/var/lib/nfsd" ||
|
||
|
|
cfg.MaxConnections != 64 {
|
||
|
|
t.Fatalf("scalars: %+v", cfg)
|
||
|
|
}
|
||
|
|
if cfg.TLS == nil || cfg.TLS.Cert != "/etc/nfsd/cert.pem" || cfg.TLS.Key != "/etc/nfsd/key.pem" {
|
||
|
|
t.Fatalf("tls: %+v", cfg.TLS)
|
||
|
|
}
|
||
|
|
if len(cfg.Exports) != 1 || cfg.Exports[0].Path != "/srv/demo" || !cfg.Exports[0].ReadOnly {
|
||
|
|
t.Fatalf("exports: %+v", cfg.Exports)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLoadConfigEmptyPath covers the explicitness rule: no -config, no
|
||
|
|
// file read, zero configuration.
|
||
|
|
func TestLoadConfigEmptyPath(t *testing.T) {
|
||
|
|
cfg, err := loadConfig("")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("load: %v", err)
|
||
|
|
}
|
||
|
|
if cfg.Listen != "" || len(cfg.Exports) != 0 {
|
||
|
|
t.Fatalf("an empty path answered %+v", cfg)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLoadConfigUnknownKey rejects a key the schema does not carry: a
|
||
|
|
// typo in the file must end the start up, not slip through.
|
||
|
|
func TestLoadConfigUnknownKey(t *testing.T) {
|
||
|
|
_, err := loadConfig(writeConfig(t, "lisn = \":2049\"\n"))
|
||
|
|
if err == nil {
|
||
|
|
t.Fatal("an unknown key was accepted")
|
||
|
|
}
|
||
|
|
var se *interpres.SyntaxError
|
||
|
|
if !errors.As(err, &se) && !strings.Contains(err.Error(), "lisn") &&
|
||
|
|
!strings.Contains(err.Error(), "unknown") {
|
||
|
|
t.Fatalf("the error names neither the key nor its kind: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLoadConfigSyntaxError reports the file and the line of a broken
|
||
|
|
// document.
|
||
|
|
func TestLoadConfigSyntaxError(t *testing.T) {
|
||
|
|
p := writeConfig(t, "listen = \":2049\"\nbroken")
|
||
|
|
_, err := loadConfig(p)
|
||
|
|
if err == nil {
|
||
|
|
t.Fatal("a broken document was accepted")
|
||
|
|
}
|
||
|
|
if !strings.Contains(err.Error(), p+":2:") {
|
||
|
|
t.Fatalf("the error misses the file or the line: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLoadConfigOneExport enforces the exactly one export contract.
|
||
|
|
func TestLoadConfigOneExport(t *testing.T) {
|
||
|
|
if _, err := loadConfig(writeConfig(t, "listen = \":2049\"")); err == nil {
|
||
|
|
t.Fatal("a file without an export was accepted")
|
||
|
|
}
|
||
|
|
if _, err := loadConfig(writeConfig(t,
|
||
|
|
"[[export]]\npath = \"/a\"\n\n[[export]]\npath = \"/b\"\n")); err == nil {
|
||
|
|
t.Fatal("a file with two exports was accepted")
|
||
|
|
}
|
||
|
|
if _, err := loadConfig(writeConfig(t, "[[export]]\nread-only = true\n")); err == nil {
|
||
|
|
t.Fatal("an export without a path was accepted")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// newTestFlags builds the flag set of main with the same names and
|
||
|
|
// defaults.
|
||
|
|
func newTestFlags() (addr, export *string, ro, logOps *bool, tlsCert, tlsKey, stateDir *string,
|
||
|
|
maxConns *int, fs *flag.FlagSet) {
|
||
|
|
fs = flag.NewFlagSet("nfsd-test", flag.ContinueOnError)
|
||
|
|
addr = fs.String("addr", ":2049", "")
|
||
|
|
export = fs.String("export", "", "")
|
||
|
|
ro = fs.Bool("ro", false, "")
|
||
|
|
tlsCert = fs.String("tls-cert", "", "")
|
||
|
|
tlsKey = fs.String("tls-key", "", "")
|
||
|
|
logOps = fs.Bool("log-ops", false, "")
|
||
|
|
stateDir = fs.String("state-dir", "", "")
|
||
|
|
maxConns = fs.Int("max-connections", 0, "")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestApplyConfigFillsUnsetFlags: the file is the base.
|
||
|
|
func TestApplyConfigFillsUnsetFlags(t *testing.T) {
|
||
|
|
cfg, err := loadConfig(writeConfig(t, sampleConfig))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("load: %v", err)
|
||
|
|
}
|
||
|
|
addr, export, ro, logOps, tlsCert, tlsKey, stateDir, maxConns, fs := newTestFlags()
|
||
|
|
applyConfig(cfg, fs, func(string) bool { return false })
|
||
|
|
if *addr != ":2049" || *export != "/srv/demo" || !*ro || !*logOps ||
|
||
|
|
*stateDir != "/var/lib/nfsd" || *maxConns != 64 ||
|
||
|
|
*tlsCert != "/etc/nfsd/cert.pem" || *tlsKey != "/etc/nfsd/key.pem" {
|
||
|
|
t.Fatalf("flags after the file: addr=%q export=%q ro=%v logOps=%v stateDir=%q maxConns=%d tls=%q,%q",
|
||
|
|
*addr, *export, *ro, *logOps, *stateDir, *maxConns, *tlsCert, *tlsKey)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestApplyConfigFlagsWin: the flags override the file.
|
||
|
|
func TestApplyConfigFlagsWin(t *testing.T) {
|
||
|
|
cfg, err := loadConfig(writeConfig(t, sampleConfig))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("load: %v", err)
|
||
|
|
}
|
||
|
|
addr, export, ro, logOps, tlsCert, tlsKey, stateDir, maxConns, fs := newTestFlags()
|
||
|
|
fs.Parse([]string{"-addr", ":9999", "-export", "/other", "-ro=false", "-log-ops=false"})
|
||
|
|
given := make(map[string]bool)
|
||
|
|
fs.Visit(func(f *flag.Flag) { given[f.Name] = true })
|
||
|
|
applyConfig(cfg, fs, func(name string) bool { return given[name] })
|
||
|
|
if *addr != ":9999" {
|
||
|
|
t.Fatalf("addr %q, the command line must win", *addr)
|
||
|
|
}
|
||
|
|
if *export != "/other" {
|
||
|
|
t.Fatalf("export %q, the command line must win", *export)
|
||
|
|
}
|
||
|
|
if *ro {
|
||
|
|
t.Fatal("ro must stay false, the command line set it")
|
||
|
|
}
|
||
|
|
if *logOps {
|
||
|
|
t.Fatal("log-ops must stay false, the command line set it")
|
||
|
|
}
|
||
|
|
if *stateDir != "/var/lib/nfsd" || *maxConns != 64 || *tlsCert == "" || *tlsKey == "" {
|
||
|
|
t.Fatalf("the file lost the keys the command line left alone: stateDir=%q maxConns=%d tls=%q,%q",
|
||
|
|
*stateDir, *maxConns, *tlsCert, *tlsKey)
|
||
|
|
}
|
||
|
|
}
|