// Copyright (c) 2026 Petr BalvĂ­n (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) } }