Files
webhooker/internal/database/bootstrap_banner_test.go
T
sneak 44c95318da
check / check (push) Successful in 3m26s
Report a zero-length or missing database that starts empty (closes #290)
Audit of the configuration and state read from files. Three cases
carried on silently: a zero-length webhooker.db booted as a first
start without the "created a new, empty database" warning, and a
missing or zero-length per-webhook database was replaced by an empty
one with only an INFO line, losing that webhook's events and pending
deliveries unannounced. All three now log that warning with the
file's path; CreateDB, which makes a new webhook's file, does not.

The README now says how .env, the main, per-webhook and archive
databases treat an empty file, and that a damaged per-webhook database
fails that webhook alone.

Model: opus-5-5
2026-10-02 16:45:41 +00:00

146 lines
3.8 KiB
Go

package database_test
import (
"bytes"
"context"
"log/slog"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sneak.berlin/go/webhooker/internal/database"
)
// passwordField is the banner line carrying the plaintext.
const passwordField = "password: "
// bannerPassword returns the password the banner printed.
func bannerPassword(t *testing.T, out string) string {
t.Helper()
for line := range strings.SplitSeq(out, "\n") {
_, value, found := strings.Cut(line, passwordField)
if found {
return strings.TrimSpace(value)
}
}
t.Fatalf("no %q line in the banner:\n%s", passwordField, out)
return ""
}
// TestFirstBoot_PrintsTheAdminPasswordAsABanner is the bootstrap half
// of https://git.eeqj.de/sneak/webhooker/issues/208.
//
// The password is shown exactly once, and it used to be shown as one
// slog record among the roughly 45 fx PROVIDE/RUN/HOOK lines a boot
// writes — which is how deployments lost it and, with no reset path,
// locked themselves out. It must be emitted as a block an operator can
// find by eye, it must carry the plaintext that actually opens the
// account, and it must name the command that recovers it.
func TestFirstBoot_PrintsTheAdminPasswordAsABanner(t *testing.T) {
t.Parallel()
db, lc := setupTestDB(t)
var out bytes.Buffer
db.ExportSetBannerOut(&out)
ctx := context.Background()
require.NoError(t, lc.Start(ctx))
defer func() { require.NoError(t, lc.Stop(ctx)) }()
printed := out.String()
require.Contains(
t, printed, strings.Repeat("=", 20),
"the banner must be ruled off, not read as one more log line",
)
require.Contains(t, printed, "username: admin")
assert.Contains(
t, printed, "resetpw",
"the banner must name the command that recovers the account",
)
password := bannerPassword(t, printed)
require.NotEmpty(t, password)
// The printed plaintext must be the one that opens the account:
// a banner showing a different string would be worse than none.
var user database.User
require.NoError(
t,
db.DB().Where("username = ?", "admin").First(&user).Error,
)
ok, err := database.VerifyPassword(password, user.Password)
require.NoError(t, err)
assert.True(
t, ok, "the printed password must open the seeded account",
)
}
// TestNewDatabase_IsLoggedWithItsPath is the log half of
// https://git.eeqj.de/sneak/webhooker/issues/359. A DATA_DIR that is
// unexpectedly empty boots exactly like a first start, so the start
// that creates the database must say so, and where. Opening that
// database again must not.
func TestNewDatabase_IsLoggedWithItsPath(t *testing.T) {
t.Parallel()
dir := t.TempDir()
open := func() string {
var out bytes.Buffer
db, err := database.Open(dir, slog.New(slog.NewTextHandler(&out, nil)))
require.NoError(t, err)
require.NoError(t, db.Close())
return out.String()
}
const created = `level=WARN msg="created a new, empty database"`
first := open()
second := open()
assert.Contains(
t, first,
created+" path="+filepath.Join(dir, database.MainDBFileName),
)
assert.NotContains(
t, second, created, "an existing database is not new",
)
}
// TestZeroLengthDatabase_IsLoggedAsNew covers what
// https://git.eeqj.de/sneak/webhooker/issues/290 found: SQLite opens a
// zero-length file as an empty database, so a start on one is a first
// start, and it must say so exactly as a start with no file does.
func TestZeroLengthDatabase_IsLoggedAsNew(t *testing.T) {
t.Parallel()
dir := t.TempDir()
path := filepath.Join(dir, database.MainDBFileName)
require.NoError(t, os.WriteFile(path, nil, database.SQLiteFilePerm))
var out bytes.Buffer
db, err := database.Open(dir, slog.New(slog.NewTextHandler(&out, nil)))
require.NoError(t, err)
require.NoError(t, db.Close())
assert.Contains(
t, out.String(),
`level=WARN msg="created a new, empty database" path=`+path,
)
}