package main
import (
"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)
}
}
// 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")
}
}