A SimpleX Chat bot that answers arithmetic (closes #1)
check / check (push) Successful in 54s
check / check (push) Successful in 54s
Remove the template's HTTP service, database and fx wiring. Add exact arithmetic on go/parser and go/constant, a client that runs simplex-chat as a child process and drives its WebSocket API, and the bot, which keeps an auto-accepting address and replies to each message. The image adds the checksum-pinned simplex-chat v7.0.2 on Ubuntu 22.04. Model: opus-5-5
This commit is contained in:
+37
-198
@@ -2,20 +2,12 @@ package config_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"sneak.berlin/go/simplexcalc/internal/config"
|
||||
)
|
||||
|
||||
// Credentials used by the metrics-auth cases.
|
||||
const (
|
||||
testUser = "scraper"
|
||||
testPass = "hunter2"
|
||||
)
|
||||
|
||||
// env builds a viper instance holding exactly the given keys, so a test
|
||||
// describes one environment without touching the process's.
|
||||
func env(kv map[string]string) *viper.Viper {
|
||||
@@ -33,63 +25,40 @@ func env(kv map[string]string) *viper.Viper {
|
||||
func TestAbsentValuesTakeDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c, err := config.Load(env(nil))
|
||||
if err != nil {
|
||||
t.Fatalf("empty environment must be valid, got: %v", err)
|
||||
}
|
||||
|
||||
if c.Port != int(config.DefaultPort) {
|
||||
t.Errorf("Port = %d, want %d", c.Port, config.DefaultPort)
|
||||
}
|
||||
|
||||
if c.MaxRequestBody != config.DefaultMaxRequestBody {
|
||||
t.Errorf("MaxRequestBody = %d, want %d",
|
||||
c.MaxRequestBody, config.DefaultMaxRequestBody)
|
||||
}
|
||||
|
||||
if c.RequestTimeout != config.DefaultRequestTimeout {
|
||||
t.Errorf("RequestTimeout = %s, want %s",
|
||||
c.RequestTimeout, config.DefaultRequestTimeout)
|
||||
}
|
||||
|
||||
if !c.HSTS {
|
||||
t.Error("HSTS must default on when DEBUG is not set")
|
||||
}
|
||||
|
||||
if !c.CSRFKeyEphemeral {
|
||||
t.Error("an absent CSRF_KEY must mark the config for an ephemeral key")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetButUnparseableAborts is the central contract of this package.
|
||||
// Every case is a value an operator plausibly types, and every one of
|
||||
// them must fail startup rather than be replaced by the default.
|
||||
func TestSetButUnparseableAborts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := map[string]map[string]string{
|
||||
"port is not a number": {config.EnvPort: "eighty"},
|
||||
"port is zero": {config.EnvPort: "0"},
|
||||
"port is above the range": {config.EnvPort: "70000"},
|
||||
"port is a float": {config.EnvPort: "8080.0"},
|
||||
"debug is yes": {config.EnvDebug: "yes"},
|
||||
"hsts is on": {config.EnvHSTS: "on"},
|
||||
"body cap is nonsense": {config.EnvMaxRequestBody: "big"},
|
||||
"body cap is too large": {config.EnvMaxRequestBody: "1TiB"},
|
||||
"body cap is too small": {config.EnvMaxRequestBody: "10"},
|
||||
"timeout has no unit": {config.EnvRequestTimeout: "30"},
|
||||
"timeout is out of range": {config.EnvRequestTimeout: "1h"},
|
||||
"grace is nonsense": {config.EnvShutdownGrace: "soon"},
|
||||
"sentry dsn is not a url": {config.EnvSentryDSN: "not a dsn"},
|
||||
"csrf key is not hex": {config.EnvCSRFKey: "not-hex-at-all"},
|
||||
"csrf key is wrong length": {config.EnvCSRFKey: "abcdef"},
|
||||
}
|
||||
|
||||
for name, kv := range cases {
|
||||
for name, kv := range map[string]map[string]string{
|
||||
"unset": nil,
|
||||
"whitespace only": {config.EnvDataDir: " ", config.EnvDebug: " "},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c, err := config.Load(env(kv))
|
||||
if err != nil {
|
||||
t.Fatalf("absent values must be valid, got: %v", err)
|
||||
}
|
||||
|
||||
if c.DataDir != config.DefaultDataDir {
|
||||
t.Errorf("DataDir = %q, want %q", c.DataDir, config.DefaultDataDir)
|
||||
}
|
||||
|
||||
if c.Debug {
|
||||
t.Error("Debug must default off")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetButUnparseableAborts is the central contract of this package:
|
||||
// a value an operator plausibly types, and that does not parse, fails
|
||||
// startup rather than being replaced by the default.
|
||||
func TestSetButUnparseableAborts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, raw := range []string{"yes", "on", "enabled", "2"} {
|
||||
t.Run(raw, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c, err := config.Load(env(map[string]string{config.EnvDebug: raw}))
|
||||
if err == nil {
|
||||
t.Fatalf("wanted a startup failure, got a Config: %+v", c)
|
||||
}
|
||||
@@ -105,153 +74,23 @@ func TestSetButUnparseableAborts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSecretsAreNotEchoed: a rejected CSRF key must not appear in the
|
||||
// error, because errors are logged and a log is not a place to put a
|
||||
// key.
|
||||
func TestSecretsAreNotEchoed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const secret = "00112233445566778899aabbccdd" // valid hex, wrong length
|
||||
|
||||
_, err := config.Load(env(map[string]string{config.EnvCSRFKey: secret}))
|
||||
if err == nil {
|
||||
t.Fatal("wanted a failure for a short CSRF key")
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), secret) {
|
||||
t.Errorf("the rejected key was echoed in the error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHalfSetMetricsAuthAborts covers the case the issue calls out
|
||||
// explicitly: auth config that is half-set must fail loudly, in both
|
||||
// directions.
|
||||
func TestHalfSetMetricsAuthAborts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := map[string]map[string]string{
|
||||
"user without password": {config.EnvMetricsUser: testUser},
|
||||
"password without user": {config.EnvMetricsPassword: testPass},
|
||||
}
|
||||
|
||||
for name, kv := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.Load(env(kv))
|
||||
if err == nil {
|
||||
t.Fatal("half-set metrics credentials must abort startup")
|
||||
}
|
||||
|
||||
if !errors.Is(err, config.ErrInvalidConfig) {
|
||||
t.Errorf("error does not wrap ErrInvalidConfig: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
both, err := config.Load(env(map[string]string{
|
||||
config.EnvMetricsUser: testUser, config.EnvMetricsPassword: testPass,
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("both credentials set must be valid, got: %v", err)
|
||||
}
|
||||
|
||||
if both.MetricsUser != testUser || both.MetricsPassword != testPass {
|
||||
t.Error("credentials did not survive parsing")
|
||||
}
|
||||
|
||||
neither, err := config.Load(env(nil))
|
||||
if err != nil {
|
||||
t.Fatalf("neither credential set must be valid, got: %v", err)
|
||||
}
|
||||
|
||||
if neither.MetricsUser != "" || neither.MetricsPassword != "" {
|
||||
t.Error("credentials appeared from nowhere")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEveryFailureIsReported: one restart should surface the whole list,
|
||||
// not just the first problem.
|
||||
func TestEveryFailureIsReported(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.Load(env(map[string]string{
|
||||
config.EnvPort: "eighty",
|
||||
config.EnvDebug: "yes",
|
||||
config.EnvRequestTimeout: "soon",
|
||||
}))
|
||||
if err == nil {
|
||||
t.Fatal("wanted failures")
|
||||
}
|
||||
|
||||
for _, key := range []string{
|
||||
config.EnvPort, config.EnvDebug, config.EnvRequestTimeout,
|
||||
} {
|
||||
if !strings.Contains(err.Error(), key) {
|
||||
t.Errorf("%s is broken but is not named in the error: %v", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidValuesAreUsed proves the parsers accept what they document.
|
||||
func TestValidValuesAreUsed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
|
||||
c, err := config.Load(env(map[string]string{
|
||||
config.EnvPort: "9000",
|
||||
config.EnvDebug: "true",
|
||||
config.EnvMaxRequestBody: "2MiB",
|
||||
config.EnvRequestTimeout: "45s",
|
||||
config.EnvShutdownGrace: "5s",
|
||||
config.EnvDataDir: "/var/lib/example",
|
||||
config.EnvCSRFKey: key,
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("valid environment was rejected: %v", err)
|
||||
}
|
||||
|
||||
if c.Port != 9000 {
|
||||
t.Errorf("Port = %d, want 9000", c.Port)
|
||||
}
|
||||
|
||||
if c.MaxRequestBody != 2<<20 {
|
||||
t.Errorf("MaxRequestBody = %d, want %d", c.MaxRequestBody, 2<<20)
|
||||
}
|
||||
|
||||
if c.RequestTimeout != 45*time.Second {
|
||||
t.Errorf("RequestTimeout = %s, want 45s", c.RequestTimeout)
|
||||
}
|
||||
|
||||
if c.HSTS {
|
||||
t.Error("HSTS must default off when DEBUG is true")
|
||||
}
|
||||
|
||||
if len(c.CSRFKey) != config.CSRFKeyBytes || c.CSRFKeyEphemeral {
|
||||
t.Errorf("CSRFKey not decoded: len=%d ephemeral=%v",
|
||||
len(c.CSRFKey), c.CSRFKeyEphemeral)
|
||||
}
|
||||
|
||||
if c.DBPath != "/var/lib/example/simplexcalc.db" {
|
||||
t.Errorf("DBPath = %q, want it derived from DATA_DIR", c.DBPath)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExplicitOverridesDerivedDBPath: DB_PATH wins over the DATA_DIR
|
||||
// derivation, which is the only reason it exists.
|
||||
func TestExplicitOverridesDerivedDBPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c, err := config.Load(env(map[string]string{
|
||||
config.EnvDataDir: "/var/lib/example",
|
||||
config.EnvDBPath: "/srv/other.db",
|
||||
config.EnvDebug: "true",
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("valid environment was rejected: %v", err)
|
||||
}
|
||||
|
||||
if c.DBPath != "/srv/other.db" {
|
||||
t.Errorf("DBPath = %q, want /srv/other.db", c.DBPath)
|
||||
if c.DataDir != "/var/lib/example" {
|
||||
t.Errorf("DataDir = %q, want /var/lib/example", c.DataDir)
|
||||
}
|
||||
|
||||
if !c.Debug {
|
||||
t.Error("Debug = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user