feat(check,serve): Startvalidierung, HTTPS-Server, Signal-Handling und Versionscheck
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
This commit is contained in:
parent
7e23df9fec
commit
2d223c5823
9 changed files with 1158 additions and 6 deletions
239
internal/check/check.go
Normal file
239
internal/check/check.go
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
// Package check prüft beim Start und über das check-Kommando die gesamte
|
||||
// Wirkungskette: Konfiguration, Dateirechte, Firewall, Verzeichnis, Uhrzeit.
|
||||
package check
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.ravensburg.dev/cabele/opnsense-portal/internal/auth"
|
||||
"git.ravensburg.dev/cabele/opnsense-portal/internal/config"
|
||||
"git.ravensburg.dev/cabele/opnsense-portal/internal/opnsense"
|
||||
)
|
||||
|
||||
// maxClockSkew ist die tolerierte Abweichung zur Firewall-Uhrzeit.
|
||||
// Eine falsche Systemzeit würde abgelaufene Zertifikate falsch bewerten.
|
||||
const maxClockSkew = 2 * time.Minute
|
||||
|
||||
// Result ist das Ergebnis eines Prüfpunkts.
|
||||
type Result struct {
|
||||
Name string
|
||||
OK bool
|
||||
Detail string
|
||||
}
|
||||
|
||||
// Report bündelt alle Prüfergebnisse.
|
||||
type Report struct {
|
||||
Results []Result
|
||||
}
|
||||
|
||||
// OK meldet, ob alle Prüfungen bestanden wurden.
|
||||
func (r *Report) OK() bool {
|
||||
for _, res := range r.Results {
|
||||
if !res.OK {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// WriteTo gibt die Checkliste aus.
|
||||
func (r *Report) WriteTo(w io.Writer) (int64, error) {
|
||||
var total int64
|
||||
for _, res := range r.Results {
|
||||
mark := "✓"
|
||||
if !res.OK {
|
||||
mark = "✗"
|
||||
}
|
||||
line := fmt.Sprintf("%s %s", mark, res.Name)
|
||||
if res.Detail != "" {
|
||||
line += "\n " + strings.ReplaceAll(res.Detail, "\n", "\n ")
|
||||
}
|
||||
n, err := io.WriteString(w, line+"\n")
|
||||
total += int64(n)
|
||||
if err != nil {
|
||||
return total, err
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// Pinger ist der benötigte Ausschnitt des Firewall-Clients.
|
||||
type Pinger interface {
|
||||
Ping(ctx context.Context) (time.Time, error)
|
||||
}
|
||||
|
||||
// DirectoryChecker ist der benötigte Ausschnitt des AD-Authenticators.
|
||||
type DirectoryChecker interface {
|
||||
ResolveGroupDN(ctx context.Context) (string, error)
|
||||
Lookup(ctx context.Context, username string) (*auth.LookupResult, error)
|
||||
}
|
||||
|
||||
// Checker führt die Prüfungen aus.
|
||||
type Checker struct {
|
||||
Cfg *config.Config
|
||||
ConfigPath string
|
||||
FW Pinger
|
||||
Dir DirectoryChecker
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
// Run führt alle Prüfungen aus. testAuthUser leer = keine Testanmeldung.
|
||||
func (c *Checker) Run(ctx context.Context, testAuthUser string) *Report {
|
||||
now := c.Now
|
||||
if now == nil {
|
||||
now = time.Now
|
||||
}
|
||||
rep := &Report{}
|
||||
add := func(name string, err error, detail string) {
|
||||
if err != nil {
|
||||
rep.Results = append(rep.Results, Result{Name: name, OK: false, Detail: err.Error()})
|
||||
return
|
||||
}
|
||||
rep.Results = append(rep.Results, Result{Name: name, OK: true, Detail: detail})
|
||||
}
|
||||
|
||||
// 1. Konfiguration
|
||||
add("Konfiguration", c.Cfg.Validate(), "gelesen und plausibel")
|
||||
|
||||
// 2. Dateirechte
|
||||
add("Dateirechte", c.checkPermissions(),
|
||||
"Konfiguration, Secrets und Log-Verzeichnis sind ausreichend geschützt")
|
||||
|
||||
// 3. OPNsense
|
||||
serverTime, fwErr := c.checkFirewall(ctx)
|
||||
add("OPNsense-Verbindung", fwErr, "Export-API erreichbar, Key und Berechtigung in Ordnung")
|
||||
|
||||
// 4. Verzeichnisdienst
|
||||
groupDN, dirErr := c.checkDirectory(ctx)
|
||||
add("Verzeichnisdienst", dirErr,
|
||||
"Anmeldung des Dienstkontos erfolgreich, Gruppe aufgelöst: "+groupDN)
|
||||
|
||||
// 5. Systemzeit (nur sinnvoll, wenn die Firewall geantwortet hat)
|
||||
add("Systemzeit", checkClock(now(), serverTime, fwErr),
|
||||
"Abweichung zur Firewall unterhalb der Schwelle")
|
||||
|
||||
// 6. Optionale Testanmeldung
|
||||
if testAuthUser != "" {
|
||||
detail, err := c.checkTestAuth(ctx, testAuthUser)
|
||||
add("Testanmeldung "+testAuthUser, err, detail)
|
||||
}
|
||||
return rep
|
||||
}
|
||||
|
||||
// checkPermissions prüft Config, Secret-Dateien, TLS-Key und Log-Verzeichnis.
|
||||
func (c *Checker) checkPermissions() error {
|
||||
var errs []error
|
||||
if c.ConfigPath != "" {
|
||||
if err := config.CheckFileMode(c.ConfigPath, 0o640); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
for _, p := range []string{
|
||||
c.Cfg.OPNsense.APIKeyFile, c.Cfg.OPNsense.APISecretFile, c.Cfg.AD.BindPasswordFile,
|
||||
} {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
if err := config.CheckFileMode(p, 0o640); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
// Der private TLS-Schlüssel darf niemandem außer dem Eigentümer offenstehen.
|
||||
if c.Cfg.Portal.TLSKey != "" {
|
||||
if err := config.CheckFileMode(c.Cfg.Portal.TLSKey, 0o600); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if c.Cfg.Logging.AuditLog != "" {
|
||||
dir := filepath.Dir(c.Cfg.Logging.AuditLog)
|
||||
if _, err := os.Stat(dir); err == nil {
|
||||
if err := config.CheckDirMode(dir, 0o750); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
} else {
|
||||
errs = append(errs, fmt.Errorf("Log-Verzeichnis %s existiert nicht: %w", dir, err))
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// checkFirewall ruft den Export-Endpunkt auf und übersetzt die Fehler in
|
||||
// handlungsleitende Meldungen.
|
||||
func (c *Checker) checkFirewall(ctx context.Context) (time.Time, error) {
|
||||
if c.FW == nil {
|
||||
return time.Time{}, errors.New("kein Firewall-Client konfiguriert")
|
||||
}
|
||||
t, err := c.FW.Ping(ctx)
|
||||
switch {
|
||||
case err == nil:
|
||||
return t, nil
|
||||
case errors.Is(err, opnsense.ErrUnauthorized):
|
||||
return time.Time{}, fmt.Errorf(
|
||||
"die Zugangsdaten wurden abgelehnt — opnsense.api_key und api_secret prüfen (%v)", err)
|
||||
case errors.Is(err, opnsense.ErrForbidden):
|
||||
return time.Time{}, fmt.Errorf(
|
||||
"der API-Benutzer hat keine Berechtigung — auf der OPNsense das Privileg "+
|
||||
"\"VPN: OpenVPN Client Export\" zuweisen (%v)", err)
|
||||
case errors.Is(err, opnsense.ErrUnreachable):
|
||||
return time.Time{}, fmt.Errorf(
|
||||
"die Firewall ist nicht erreichbar — URL, Netzwerkweg und CA prüfen (%v)", err)
|
||||
default:
|
||||
return time.Time{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// checkDirectory bindet den Service-Account und löst die Gruppe auf.
|
||||
func (c *Checker) checkDirectory(ctx context.Context) (string, error) {
|
||||
if c.Dir == nil {
|
||||
return "", errors.New("kein Verzeichnis-Client konfiguriert")
|
||||
}
|
||||
dn, err := c.Dir.ResolveGroupDN(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Verzeichnisdienst nicht nutzbar: %w", err)
|
||||
}
|
||||
if dn == "" {
|
||||
return "", fmt.Errorf("Gruppe %q konnte nicht aufgelöst werden", c.Cfg.AD.VPNGroup)
|
||||
}
|
||||
return dn, nil
|
||||
}
|
||||
|
||||
// checkClock vergleicht lokale Zeit und Firewall-Zeit.
|
||||
func checkClock(local, remote time.Time, fwErr error) error {
|
||||
if fwErr != nil {
|
||||
return errors.New("nicht prüfbar, weil die Firewall nicht geantwortet hat")
|
||||
}
|
||||
if remote.IsZero() {
|
||||
return nil // Die Firewall hat keinen auswertbaren Date-Header geliefert.
|
||||
}
|
||||
skew := local.Sub(remote)
|
||||
if skew < 0 {
|
||||
skew = -skew
|
||||
}
|
||||
if skew > maxClockSkew {
|
||||
return fmt.Errorf(
|
||||
"die Systemzeit weicht um %s von der Firewall ab — bitte NTP einrichten "+
|
||||
"(abgelaufene Zertifikate würden sonst falsch bewertet)", skew.Round(time.Second))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkTestAuth spielt Suche und Gruppenprüfung ohne Passwort durch.
|
||||
func (c *Checker) checkTestAuth(ctx context.Context, username string) (string, error) {
|
||||
res, err := c.Dir.Lookup(ctx, username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !res.InVPNGroup {
|
||||
return "", fmt.Errorf("DN: %s\nsAMAccountName: %s\nMitglied in %s: NEIN — "+
|
||||
"dieser Benutzer würde abgewiesen", res.DN, res.SAMAccountName, c.Cfg.AD.VPNGroup)
|
||||
}
|
||||
return fmt.Sprintf("DN: %s\nsAMAccountName: %s\nMitglied in %s: ja",
|
||||
res.DN, res.SAMAccountName, c.Cfg.AD.VPNGroup), nil
|
||||
}
|
||||
260
internal/check/check_test.go
Normal file
260
internal/check/check_test.go
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
package check
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.ravensburg.dev/cabele/opnsense-portal/internal/auth"
|
||||
"git.ravensburg.dev/cabele/opnsense-portal/internal/config"
|
||||
"git.ravensburg.dev/cabele/opnsense-portal/internal/opnsense"
|
||||
)
|
||||
|
||||
type fakePinger struct {
|
||||
at time.Time
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakePinger) Ping(ctx context.Context) (time.Time, error) { return f.at, f.err }
|
||||
|
||||
type fakeDir struct {
|
||||
dn string
|
||||
dnErr error
|
||||
lookup *auth.LookupResult
|
||||
lookErr error
|
||||
}
|
||||
|
||||
func (f *fakeDir) ResolveGroupDN(ctx context.Context) (string, error) { return f.dn, f.dnErr }
|
||||
func (f *fakeDir) Lookup(ctx context.Context, u string) (*auth.LookupResult, error) {
|
||||
return f.lookup, f.lookErr
|
||||
}
|
||||
|
||||
var checkNow = time.Date(2026, 8, 14, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
func newChecker(t *testing.T, fw Pinger, dir DirectoryChecker) *Checker {
|
||||
t.Helper()
|
||||
tmp := t.TempDir()
|
||||
cfgPath := filepath.Join(tmp, "config.yaml")
|
||||
if err := os.WriteFile(cfgPath, []byte("x"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Chmod(cfgPath, 0o600)
|
||||
|
||||
logDir := filepath.Join(tmp, "log")
|
||||
os.MkdirAll(logDir, 0o750)
|
||||
os.Chmod(logDir, 0o750)
|
||||
|
||||
cfg := config.Defaults()
|
||||
cfg.Portal.TLSCert = filepath.Join(tmp, "portal.crt")
|
||||
cfg.Portal.TLSKey = filepath.Join(tmp, "portal.key")
|
||||
os.WriteFile(cfg.Portal.TLSCert, []byte("cert"), 0o644)
|
||||
os.WriteFile(cfg.Portal.TLSKey, []byte("key"), 0o600)
|
||||
os.Chmod(cfg.Portal.TLSKey, 0o600)
|
||||
cfg.OPNsense.URL = "https://fw01.firma.local"
|
||||
cfg.OPNsense.APIKey, cfg.OPNsense.APISecret = "k", "s"
|
||||
cfg.AD.Domain = "firma.local"
|
||||
cfg.AD.Servers = []string{"dc01.firma.local"}
|
||||
cfg.AD.BindUser, cfg.AD.BindPassword = "svc@firma.local", "pw"
|
||||
cfg.AD.VPNGroup = "VPN-Users"
|
||||
cfg.Logging.AuditLog = filepath.Join(logDir, "audit.log")
|
||||
|
||||
return &Checker{Cfg: cfg, ConfigPath: cfgPath, FW: fw, Dir: dir,
|
||||
Now: func() time.Time { return checkNow }}
|
||||
}
|
||||
|
||||
func report(t *testing.T, rep *Report) string {
|
||||
t.Helper()
|
||||
var b strings.Builder
|
||||
if _, err := rep.WriteTo(&b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func TestAllChecksPass(t *testing.T) {
|
||||
c := newChecker(t, &fakePinger{at: checkNow},
|
||||
&fakeDir{dn: "CN=VPN-Users,DC=firma,DC=local"})
|
||||
rep := c.Run(context.Background(), "")
|
||||
|
||||
if !rep.OK() {
|
||||
t.Fatalf("alle Prüfungen sollten bestehen:\n%s", report(t, rep))
|
||||
}
|
||||
out := report(t, rep)
|
||||
for _, want := range []string{"Konfiguration", "Dateirechte", "OPNsense", "Verzeichnisdienst", "Systemzeit"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("Prüfpunkt %q fehlt:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailedAPISecretIsNamed(t *testing.T) {
|
||||
c := newChecker(t, &fakePinger{err: opnsense.ErrUnauthorized},
|
||||
&fakeDir{dn: "CN=VPN-Users,DC=firma,DC=local"})
|
||||
rep := c.Run(context.Background(), "")
|
||||
|
||||
if rep.OK() {
|
||||
t.Fatal("falsches Secret muss auffallen")
|
||||
}
|
||||
out := report(t, rep)
|
||||
if !strings.Contains(out, "✗") {
|
||||
t.Errorf("Ausgabe muss ein ✗ enthalten:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(out), "zugangsdaten") {
|
||||
t.Errorf("Ursache muss benannt werden:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingAPIPermissionIsDistinguished(t *testing.T) {
|
||||
c := newChecker(t, &fakePinger{err: opnsense.ErrForbidden}, &fakeDir{dn: "CN=x"})
|
||||
rep := c.Run(context.Background(), "")
|
||||
out := report(t, rep)
|
||||
if !strings.Contains(out, "Client Export") {
|
||||
t.Errorf("fehlende Berechtigung muss den nötigen Privilegnamen nennen:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnreachableDCIsNamed(t *testing.T) {
|
||||
c := newChecker(t, &fakePinger{at: checkNow},
|
||||
&fakeDir{dnErr: errors.New("dc01.firma.local: connection refused")})
|
||||
rep := c.Run(context.Background(), "")
|
||||
if rep.OK() {
|
||||
t.Fatal("nicht erreichbarer DC muss auffallen")
|
||||
}
|
||||
if out := report(t, rep); !strings.Contains(out, "dc01.firma.local") {
|
||||
t.Errorf("betroffener DC muss genannt werden:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLooseConfigPermissionsAreDetected(t *testing.T) {
|
||||
c := newChecker(t, &fakePinger{at: checkNow}, &fakeDir{dn: "CN=x"})
|
||||
os.Chmod(c.ConfigPath, 0o644)
|
||||
rep := c.Run(context.Background(), "")
|
||||
if rep.OK() {
|
||||
t.Fatal("0644-Config muss auffallen")
|
||||
}
|
||||
if out := report(t, rep); !strings.Contains(out, "chmod") {
|
||||
t.Errorf("Reparaturbefehl muss genannt werden:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLooseTLSKeyIsDetected(t *testing.T) {
|
||||
c := newChecker(t, &fakePinger{at: checkNow}, &fakeDir{dn: "CN=x"})
|
||||
os.Chmod(c.Cfg.Portal.TLSKey, 0o644)
|
||||
rep := c.Run(context.Background(), "")
|
||||
if rep.OK() {
|
||||
t.Fatal("world-readable TLS-Key muss auffallen")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingLogDirectoryIsDetected(t *testing.T) {
|
||||
c := newChecker(t, &fakePinger{at: checkNow}, &fakeDir{dn: "CN=x"})
|
||||
c.Cfg.Logging.AuditLog = "/gibt/es/nicht/audit.log"
|
||||
rep := c.Run(context.Background(), "")
|
||||
if rep.OK() {
|
||||
t.Fatal("fehlendes Log-Verzeichnis muss auffallen")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClockSkewWarns(t *testing.T) {
|
||||
c := newChecker(t, &fakePinger{at: checkNow.Add(5 * time.Minute)}, &fakeDir{dn: "CN=x"})
|
||||
rep := c.Run(context.Background(), "")
|
||||
out := report(t, rep)
|
||||
if !strings.Contains(out, "NTP") {
|
||||
t.Errorf("Zeitabweichung muss auf NTP hinweisen:\n%s", out)
|
||||
}
|
||||
if rep.OK() {
|
||||
t.Error("Zeitabweichung über der Schwelle muss als Fehler zählen")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSmallClockSkewIsFine(t *testing.T) {
|
||||
c := newChecker(t, &fakePinger{at: checkNow.Add(30 * time.Second)},
|
||||
&fakeDir{dn: "CN=VPN-Users,DC=firma,DC=local"})
|
||||
if rep := c.Run(context.Background(), ""); !rep.OK() {
|
||||
t.Fatalf("30 Sekunden Abweichung sind unkritisch:\n%s", report(t, rep))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClockCheckSkippedWhenFirewallDown(t *testing.T) {
|
||||
// Ohne Antwort der Firewall lässt sich die Uhr nicht vergleichen; das darf
|
||||
// nicht als eigenständiger Zeitfehler dastehen.
|
||||
c := newChecker(t, &fakePinger{err: opnsense.ErrUnreachable}, &fakeDir{dn: "CN=x"})
|
||||
rep := c.Run(context.Background(), "")
|
||||
out := report(t, rep)
|
||||
if !strings.Contains(out, "nicht prüfbar") {
|
||||
t.Errorf("Zeitprüfung muss als nicht prüfbar ausgewiesen werden:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestAuthShowsLookupResult(t *testing.T) {
|
||||
dir := &fakeDir{
|
||||
dn: "CN=VPN-Users,DC=firma,DC=local",
|
||||
lookup: &auth.LookupResult{
|
||||
DN: "CN=Max Mueller,OU=Users,DC=firma,DC=local", SAMAccountName: "mmueller", InVPNGroup: true},
|
||||
}
|
||||
c := newChecker(t, &fakePinger{at: checkNow}, dir)
|
||||
rep := c.Run(context.Background(), "mmueller")
|
||||
|
||||
out := report(t, rep)
|
||||
for _, want := range []string{"CN=Max Mueller", "mmueller", "VPN-Users"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("Ausgabe von --test-auth enthält %q nicht:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestAuthReportsMissingGroupMembership(t *testing.T) {
|
||||
dir := &fakeDir{
|
||||
dn: "CN=VPN-Users,DC=firma,DC=local",
|
||||
lookup: &auth.LookupResult{DN: "CN=J Doe", SAMAccountName: "jdoe", InVPNGroup: false},
|
||||
}
|
||||
c := newChecker(t, &fakePinger{at: checkNow}, dir)
|
||||
rep := c.Run(context.Background(), "jdoe")
|
||||
if rep.OK() {
|
||||
t.Fatal("fehlende Gruppenmitgliedschaft muss als Fehler gelten")
|
||||
}
|
||||
if out := report(t, rep); !strings.Contains(out, "NEIN") {
|
||||
t.Errorf("das Ergebnis muss deutlich benannt werden:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestAuthSkippedWhenEmpty(t *testing.T) {
|
||||
c := newChecker(t, &fakePinger{at: checkNow}, &fakeDir{dn: "CN=x"})
|
||||
rep := c.Run(context.Background(), "")
|
||||
if strings.Contains(report(t, rep), "Testanmeldung") {
|
||||
t.Error("ohne --test-auth darf kein Testanmelde-Punkt erscheinen")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportOutputFormat(t *testing.T) {
|
||||
rep := &Report{Results: []Result{
|
||||
{Name: "Konfiguration", OK: true},
|
||||
{Name: "Dateirechte", OK: false, Detail: "config.yaml ist 0644"},
|
||||
}}
|
||||
out := report(t, rep)
|
||||
if !strings.Contains(out, "✓ Konfiguration") {
|
||||
t.Errorf("Erfolgszeile falsch:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "✗ Dateirechte") || !strings.Contains(out, "config.yaml ist 0644") {
|
||||
t.Errorf("Fehlerzeile falsch:\n%s", out)
|
||||
}
|
||||
if rep.OK() {
|
||||
t.Error("ein ✗ muss OK() falsch machen")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidConfigIsReported(t *testing.T) {
|
||||
c := newChecker(t, &fakePinger{at: checkNow}, &fakeDir{dn: "CN=x"})
|
||||
c.Cfg.AD.Servers = []string{"10.1.1.10"} // IP statt Hostname
|
||||
rep := c.Run(context.Background(), "")
|
||||
if rep.OK() {
|
||||
t.Fatal("ungültige Konfiguration muss auffallen")
|
||||
}
|
||||
if out := report(t, rep); !strings.Contains(out, "10.1.1.10") {
|
||||
t.Errorf("der beanstandete Wert muss genannt werden:\n%s", out)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue