check / check (push) Successful in 4m4s
A new database brings a new session key. A browser still holding the old session cookie got a 500 on a correct login: Session.Get returned the cookie's decode error and the login handler answered it with a 500. Get now treats a cookie that does not decode as absent, and logging in replaces it. gorilla/csrf already did the same for the CSRF cookie. A start that creates webhooker.db now logs "created a new, empty database" at WARN with its path, shortly before the first-boot banner, so an unexpectedly empty DATA_DIR is noticed. The codec tests now decode through the store, since Get no longer reports the codec's reason. Model: opus-5-5
122 lines
3.1 KiB
Go
122 lines
3.1 KiB
Go
package database_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"log/slog"
|
|
"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",
|
|
)
|
|
}
|