package main import ( "log/slog" "strings" "testing" "github.com/vaughan0/go-ini" "sourcecraft.dev/bigbes/sr-ht-spec/hooks" ) // completeConfig is an instance config with every key this daemon needs: the // two core-go fatals on, and everything service.LoadConfig reads. func completeConfig() ini.File { return ini.File{ "sr.ht": { "network-key": "wF4z9nZ6C0oL0kQyq3M0j0m3iSvWq7iFhk1v9E9WcBM=", "owner-name": "bigbes", "owner-email": "bigbes@example.invalid", }, "webhooks": { "private-key": "eV5B1o1M4a0dQKr2v4h0hR3vJm0m5g7jL1kQpS4Xk1s=", }, serviceName: { "origin": "https://spec.srht.bigb.es", "repos": "/var/lib/spec", "cache": "/var/cache/spec", "connection-string": "postgresql://specsrht@localhost/spec.sr.ht?sslmode=disable", }, } } func without(conf ini.File, section, key string) ini.File { out := ini.File{} for s, kv := range conf { copied := ini.Section{} for k, v := range kv { if s == section && k == key { continue } copied[k] = v } out[s] = copied } return out } func TestValidateConfigAcceptsACompleteConfig(t *testing.T) { cfg, err := validateConfig(completeConfig()) if err != nil { t.Fatalf("validateConfig: %v", err) } if cfg.Repos != "/var/lib/spec" || cfg.Cache != "/var/cache/spec" { t.Errorf("config not carried through: %+v", cfg) } if cfg.Instance.OwnerName != "bigbes" { t.Errorf("owner not carried through: %+v", cfg.Instance) } } func TestValidateConfigNamesEachMissingKey(t *testing.T) { tests := []struct { section, key, want string }{ {"sr.ht", "network-key", "[sr.ht] network-key"}, {"webhooks", "private-key", "[webhooks] private-key"}, {"sr.ht", "owner-name", "[sr.ht] owner-name"}, {"sr.ht", "owner-email", "[sr.ht] owner-email"}, {serviceName, "origin", "[spec.sr.ht] origin"}, {serviceName, "repos", "[spec.sr.ht] repos"}, {serviceName, "cache", "[spec.sr.ht] cache"}, {serviceName, "connection-string", "[spec.sr.ht] connection-string"}, } for _, tt := range tests { t.Run(tt.section+"/"+tt.key, func(t *testing.T) { _, err := validateConfig(without(completeConfig(), tt.section, tt.key)) if err == nil { t.Fatalf("validateConfig accepted a config with no %s", tt.want) } if !strings.Contains(err.Error(), tt.want) { t.Errorf("the error does not name %s:\n%v", tt.want, err) } }) } } // TestValidateConfigReportsEveryGapAtOnce is the whole point of doing this at // startup: an operator fixes the config in one pass instead of discovering // each gap on a separate restart. func TestValidateConfigReportsEveryGapAtOnce(t *testing.T) { conf := ini.File{} _, err := validateConfig(conf) if err == nil { t.Fatal("validateConfig accepted an empty config") } for _, want := range []string{ "[sr.ht] network-key", "[webhooks] private-key", "[sr.ht] owner-name", "[sr.ht] owner-email", "[spec.sr.ht] origin", "[spec.sr.ht] repos", "[spec.sr.ht] cache", "[spec.sr.ht] connection-string", } { if !strings.Contains(err.Error(), want) { t.Errorf("the error does not name %s:\n%v", want, err) } } } // TestValidateConfigRejectsAnUnusableValue: a present but wrong key is as // fatal as a missing one, and service.Config.Validate is what says so. func TestValidateConfigRejectsAnUnusableValue(t *testing.T) { conf := completeConfig() conf[serviceName]["repos"] = "var/lib/spec" // relative _, err := validateConfig(conf) if err == nil { t.Fatal("validateConfig accepted a relative repos root") } if !strings.Contains(err.Error(), "absolute path") { t.Errorf("the error does not explain the problem:\n%v", err) } } // TestBlankIsMissing: an empty value satisfies "the key is present" and // nothing else. func TestBlankIsMissing(t *testing.T) { conf := completeConfig() conf["sr.ht"]["network-key"] = " " _, err := validateConfig(conf) if err == nil { t.Fatal("validateConfig accepted a blank network-key") } if !strings.Contains(err.Error(), "[sr.ht] network-key") { t.Errorf("the error does not name the blank key:\n%v", err) } } func TestParseLevel(t *testing.T) { tests := map[string]slog.Level{ "": slog.LevelInfo, "info": slog.LevelInfo, "nonsense": slog.LevelInfo, "debug": slog.LevelDebug, " DEBUG ": slog.LevelDebug, "warn": slog.LevelWarn, "warning": slog.LevelWarn, "error": slog.LevelError, } for in, want := range tests { if got := parseLevel(in); got != want { t.Errorf("parseLevel(%q) = %v want %v", in, got, want) } } } // TestHookDispatchIsCheckedBeforeAnythingElse guards the one ordering in main // that matters: a hook must not read a config file or open Postgres, because // it runs once per ref of every push and its only job is to reach the daemon. func TestHookDispatchIsCheckedBeforeAnythingElse(t *testing.T) { if _, _, ok := hooks.ModeFromArgs([]string{"/var/lib/spec/~bigbes/rfcs/hooks/update"}); !ok { t.Error("main would treat an update hook invocation as a daemon start") } if _, _, ok := hooks.ModeFromArgs([]string{"/usr/local/bin/specsrht", "-b", "localhost:5091"}); ok { t.Error("main would treat a daemon start as a hook invocation") } }