Report or refuse each unusable file webhooker reads (closes #290)
check / check (push) Successful in 3m18s
check / check (push) Successful in 3m18s
Audit of the files webhooker reads configuration or required state from. A missing or zero-length database is reported with the "created a new, empty database" warning and its path: webhooker.db at start, and a per-webhook database at the latest at the next start, since restart recovery now opens the database of every webhook that still exists. The main database's open errors name webhooker.db (#459). webhooker resetpw refuses a zero-length webhooker.db as it refuses a missing one. A directory in place of a database file or its -wal or -shm is refused, naming it; beside a -shm directory SQLite opened the database read-only without a word. The README says how each case is treated. Model: opus-5-5
This commit is contained in:
@@ -699,10 +699,9 @@ func (e *Engine) recoverInFlight(ctx context.Context) {
|
||||
default:
|
||||
}
|
||||
|
||||
if !e.dbManager.DBExists(webhookID) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Opened even when its file is missing, so that a lost
|
||||
// database is reported at start, not when the webhook next
|
||||
// receives an event, which for a quiet webhook may be never.
|
||||
e.recoverWebhookDeliveries(ctx, webhookID)
|
||||
}
|
||||
}
|
||||
@@ -710,7 +709,24 @@ func (e *Engine) recoverInFlight(ctx context.Context) {
|
||||
func (e *Engine) recoverWebhookDeliveries(
|
||||
ctx context.Context, webhookID string,
|
||||
) {
|
||||
webhookDB, err := e.dbManager.GetDB(webhookID)
|
||||
// The web interface is already serving, so the webhook may have
|
||||
// been deleted since the list was read. Opening its database then
|
||||
// would create the file again after the delete removed it.
|
||||
stillExists := func() (bool, error) {
|
||||
var count int64
|
||||
|
||||
err := e.database.DB().
|
||||
Model(&database.Webhook{}).
|
||||
Where("id = ?", webhookID).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("confirming webhook exists: %w", err)
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
webhookDB, err := e.dbManager.GetDBIf(webhookID, stillExists)
|
||||
if err != nil {
|
||||
e.log.Error(
|
||||
"failed to get webhook database for recovery",
|
||||
@@ -721,6 +737,10 @@ func (e *Engine) recoverWebhookDeliveries(
|
||||
return
|
||||
}
|
||||
|
||||
if webhookDB == nil {
|
||||
return
|
||||
}
|
||||
|
||||
e.recoverPendingDeliveries(
|
||||
ctx, webhookDB, webhookID,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package delivery_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -1136,6 +1137,85 @@ func TestRecoverInFlight_WithPendingDeliveries(
|
||||
}
|
||||
}
|
||||
|
||||
// TestRecoverInFlight_ReportsAMissingWebhookDatabase covers a webhook
|
||||
// whose database file is gone, after a partial restore say. Restart
|
||||
// recovery opens every webhook's database, so the empty one made in its
|
||||
// place is reported at start, naming the file
|
||||
// (https://git.eeqj.de/sneak/webhooker/issues/290).
|
||||
func TestRecoverInFlight_ReportsAMissingWebhookDatabase(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mainDB := iMainDB(t)
|
||||
webhookID := uuid.New().String()
|
||||
iCreateWebhook(t, mainDB, webhookID, "lost-database")
|
||||
|
||||
var logs bytes.Buffer
|
||||
|
||||
dbMgr := database.NewTestWebhookDBManagerWithLogger(
|
||||
t.TempDir(), slog.New(slog.NewTextHandler(&logs, nil)),
|
||||
)
|
||||
t.Cleanup(func() { _ = dbMgr.CloseAll() })
|
||||
|
||||
engine := delivery.NewTestEngineWithDB(
|
||||
database.NewTestDatabase(mainDB), dbMgr,
|
||||
slog.New(slog.DiscardHandler),
|
||||
&http.Client{Timeout: 5 * time.Second}, 1,
|
||||
)
|
||||
|
||||
engine.ExportRecoverInFlight(context.Background())
|
||||
|
||||
assert.Contains(
|
||||
t, logs.String(),
|
||||
`level=WARN msg="created a new, empty database" webhook_id=`+
|
||||
webhookID+" path="+dbMgr.DBPath(webhookID),
|
||||
)
|
||||
}
|
||||
|
||||
// TestRecoverInFlight_SkipsAWebhookDeletedAfterTheListIsRead covers a
|
||||
// webhook deleted from the web interface while restart recovery runs.
|
||||
// Its database file is gone, and recovery must not create it again.
|
||||
func TestRecoverInFlight_SkipsAWebhookDeletedAfterTheListIsRead(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
mainDB := iMainDB(t)
|
||||
webhookID := uuid.New().String()
|
||||
iCreateWebhook(t, mainDB, webhookID, "deleted-during-recovery")
|
||||
|
||||
// The first query to return is recovery's read of the list of
|
||||
// webhooks. Deleting the webhook right after it puts the delete
|
||||
// between that read and the opening of the webhook's database.
|
||||
deleted := false
|
||||
|
||||
require.NoError(t, mainDB.Callback().Query().After("gorm:query").
|
||||
Register("delete-after-list", func(*gorm.DB) {
|
||||
if deleted {
|
||||
return
|
||||
}
|
||||
|
||||
deleted = true
|
||||
|
||||
require.NoError(t, mainDB.Delete(
|
||||
&database.Webhook{}, "id = ?", webhookID,
|
||||
).Error)
|
||||
}))
|
||||
|
||||
dbMgr := database.NewTestWebhookDBManager(t.TempDir())
|
||||
t.Cleanup(func() { _ = dbMgr.CloseAll() })
|
||||
|
||||
engine := delivery.NewTestEngineWithDB(
|
||||
database.NewTestDatabase(mainDB), dbMgr,
|
||||
slog.New(slog.DiscardHandler),
|
||||
&http.Client{Timeout: 5 * time.Second}, 1,
|
||||
)
|
||||
|
||||
engine.ExportRecoverInFlight(context.Background())
|
||||
|
||||
require.True(t, deleted)
|
||||
assert.False(t, dbMgr.DBExists(webhookID))
|
||||
}
|
||||
|
||||
// --- HTTP Config with custom headers ---
|
||||
|
||||
func TestDeliverHTTP_CustomTargetHeaders(t *testing.T) {
|
||||
|
||||
@@ -573,6 +573,8 @@ func TestRecoverPending_TargetDeleted(t *testing.T) {
|
||||
|
||||
s := newISetup(t)
|
||||
|
||||
iCreateWebhook(t, s.MainDB, s.WebhookID, "pending-recovery")
|
||||
|
||||
deliveryID := tSeedDeletedTarget(
|
||||
t, s, "gone-while-pending", "http://example.com/hook",
|
||||
database.DeliveryStatusPending,
|
||||
@@ -612,6 +614,8 @@ func TestRecoverPending_TargetDeleted_LeavesAnOwnedDeliveryAlone(
|
||||
|
||||
s := newISetup(t)
|
||||
|
||||
iCreateWebhook(t, s.MainDB, s.WebhookID, "owned-recovery")
|
||||
|
||||
deliveryID := tSeedDeletedTarget(
|
||||
t, s, "gone-but-owned", "http://example.com/hook",
|
||||
database.DeliveryStatusPending,
|
||||
|
||||
Reference in New Issue
Block a user