check / check (push) Successful in 3m26s
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
146 lines
3.8 KiB
Go
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,
|
|
)
|
|
}
|