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", ) }