check / check (push) Canceled after 0s
The SQLite archive files the database target writes are now managed by internal/archive: file names, rotation, the writer of each target, pruning, renames, the gzipped JSON export, the idle sweep, and the lock renames run under, which was Handlers.renameMu. The database target stays in internal/delivery and writes through archive.Writers; the engine still closes the writers when it stops, so the stop order is unchanged. The handlers take the lock, rename and evict through archive.Archives. Exported names drop their Archive prefix in the new package. The tests moved with the code; the two that check a delivery's recorded attempt stay in delivery. Model: opus-5-5
307 lines
7.4 KiB
Go
307 lines
7.4 KiB
Go
package delivery_test
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
_ "modernc.org/sqlite" // Pure Go SQLite driver.
|
|
"sneak.berlin/go/webhooker/internal/archive"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/database/databasetest"
|
|
"sneak.berlin/go/webhooker/internal/delivery"
|
|
"sneak.berlin/go/webhooker/internal/gormlog"
|
|
)
|
|
|
|
// archiveTestWebhookName is the name of every webhook
|
|
// seedDatabaseTarget creates. It is not safe in a file name as it
|
|
// stands, so every archive file name made from it is made safe first.
|
|
const archiveTestWebhookName = "Sweep Test!"
|
|
|
|
// archiveEnv bundles the pieces a database target test drives: a main
|
|
// configuration database holding webhooks and targets, a delivery
|
|
// engine whose archive writers write into the data directory, and that
|
|
// directory.
|
|
type archiveEnv struct {
|
|
eng *delivery.Engine
|
|
mainDB *database.Database
|
|
dataDir string
|
|
}
|
|
|
|
func setupArchiveTest(t *testing.T) *archiveEnv {
|
|
t.Helper()
|
|
|
|
dataDir := t.TempDir()
|
|
|
|
mainDB, err := database.Open(dataDir, slog.New(slog.DiscardHandler))
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() { _ = mainDB.Close() })
|
|
|
|
return &archiveEnv{
|
|
eng: delivery.NewTestEngineWithDB(
|
|
mainDB,
|
|
databasetest.NewWebhookDBManager(t, dataDir),
|
|
archiveTestLogger(),
|
|
&http.Client{Timeout: 5 * time.Second},
|
|
1,
|
|
),
|
|
mainDB: mainDB,
|
|
dataDir: dataDir,
|
|
}
|
|
}
|
|
|
|
func archiveTestLogger() *slog.Logger {
|
|
return slog.New(slog.NewTextHandler(
|
|
os.Stderr,
|
|
&slog.HandlerOptions{Level: slog.LevelDebug},
|
|
))
|
|
}
|
|
|
|
// archivePath returns where the engine keeps a database target's
|
|
// archive file, for the names seedDatabaseTarget gave it.
|
|
func (env *archiveEnv) archivePath(tgt *database.Target) string {
|
|
return filepath.Join(
|
|
env.dataDir,
|
|
archive.FileName(archiveTestWebhookName, tgt.Name, tgt.ID),
|
|
)
|
|
}
|
|
|
|
// seedDatabaseTarget creates a webhook with one database target
|
|
// carrying the given target config JSON, and returns the target.
|
|
func (env *archiveEnv) seedDatabaseTarget(
|
|
t *testing.T, configJSON string,
|
|
) *database.Target {
|
|
t.Helper()
|
|
|
|
wh := &database.Webhook{
|
|
UserID: uuid.New().String(),
|
|
Name: archiveTestWebhookName,
|
|
}
|
|
require.NoError(
|
|
t,
|
|
env.mainDB.DB().
|
|
Omit(clause.Associations).
|
|
Create(wh).Error,
|
|
)
|
|
|
|
tgt := &database.Target{
|
|
WebhookID: wh.ID,
|
|
Name: "Archive",
|
|
Type: database.TargetTypeDatabase,
|
|
Active: true,
|
|
Config: configJSON,
|
|
MaxAttempts: 1,
|
|
}
|
|
require.NoError(
|
|
t,
|
|
env.mainDB.DB().
|
|
Omit(clause.Associations).
|
|
Create(tgt).Error,
|
|
)
|
|
|
|
return tgt
|
|
}
|
|
|
|
// seedDatabaseTargetDelivery seeds a pending delivery of an event
|
|
// to a database target and returns the in-memory delivery the
|
|
// target handler is invoked with.
|
|
func seedDatabaseTargetDelivery(
|
|
t *testing.T,
|
|
webhookDB *gorm.DB,
|
|
event database.Event,
|
|
tgt *database.Target,
|
|
) *database.Delivery {
|
|
t.Helper()
|
|
|
|
dlv := seedDelivery(
|
|
t, webhookDB, event.ID, tgt.ID,
|
|
database.DeliveryStatusPending,
|
|
)
|
|
|
|
d := &database.Delivery{
|
|
EventID: event.ID,
|
|
TargetID: tgt.ID,
|
|
Status: database.DeliveryStatusPending,
|
|
Event: event,
|
|
Target: database.Target{
|
|
Name: tgt.Name,
|
|
Type: database.TargetTypeDatabase,
|
|
Config: tgt.Config,
|
|
},
|
|
}
|
|
d.ID = dlv.ID
|
|
|
|
return d
|
|
}
|
|
|
|
// openArchiveDBForRead opens an archive file read-only so a
|
|
// test can inspect the rows the database target archived.
|
|
func openArchiveDBForRead(
|
|
t *testing.T, path string,
|
|
) *gorm.DB {
|
|
t.Helper()
|
|
|
|
sqlDB, err := sql.Open(
|
|
"sqlite",
|
|
fmt.Sprintf("file:%s?mode=ro", path),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() { _ = sqlDB.Close() })
|
|
|
|
gdb, err := gorm.Open(
|
|
sqlite.Dialector{Conn: sqlDB},
|
|
&gorm.Config{Logger: gormlog.New(slog.New(slog.DiscardHandler))},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
return gdb
|
|
}
|
|
|
|
// countArchivedRows counts the rows in an archive file without
|
|
// asserting anything, so it is safe to poll from an
|
|
// assert.Eventually condition (which runs off the test
|
|
// goroutine, where testify assertions must not be used).
|
|
func countArchivedRows(path string) (int64, error) {
|
|
sqlDB, err := sql.Open(
|
|
"sqlite", fmt.Sprintf("file:%s?mode=ro", path),
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
defer func() { _ = sqlDB.Close() }()
|
|
|
|
gdb, err := gorm.Open(
|
|
sqlite.Dialector{Conn: sqlDB},
|
|
&gorm.Config{Logger: gormlog.New(slog.New(slog.DiscardHandler))},
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var count int64
|
|
|
|
err = gdb.Table("archived_events").Count(&count).Error
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// archivedRow is the columns of an archive row these tests read.
|
|
type archivedRow struct {
|
|
EventID string
|
|
WebhookID string
|
|
Method string
|
|
RawQuery string
|
|
Body string
|
|
}
|
|
|
|
// TestDeliverDatabase_ArchivesEvent verifies that delivering to
|
|
// a database target marks the delivery delivered and archives
|
|
// the full event into the target's own archive file.
|
|
func TestDeliverDatabase_ArchivesEvent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupArchiveTest(t)
|
|
tgt := env.seedDatabaseTarget(t, "")
|
|
|
|
webhookDB := testWebhookDB(t)
|
|
event := seedEvent(t, webhookDB, `{"archived":true}`)
|
|
event.RawQuery = eventQuery
|
|
d := seedDatabaseTargetDelivery(t, webhookDB, event, tgt)
|
|
|
|
env.eng.ExportDeliverDatabase(webhookDB, d)
|
|
|
|
var updated database.Delivery
|
|
|
|
require.NoError(t, webhookDB.First(
|
|
&updated, "id = ?", d.ID,
|
|
).Error)
|
|
assert.Equal(t,
|
|
database.DeliveryStatusDelivered, updated.Status,
|
|
"database target should mark the delivery delivered",
|
|
)
|
|
|
|
archivePath := filepath.Join(
|
|
env.dataDir, "archive-sweep-test-archive-"+tgt.ID+".db",
|
|
)
|
|
assert.FileExists(t, archivePath)
|
|
|
|
rdb := openArchiveDBForRead(t, archivePath)
|
|
|
|
var rows []archivedRow
|
|
|
|
require.NoError(t, rdb.Table("archived_events").Find(&rows).Error)
|
|
require.Len(t, rows, 1)
|
|
assert.Equal(t, event.ID, rows[0].EventID)
|
|
assert.Equal(t, event.WebhookID, rows[0].WebhookID)
|
|
assert.Equal(t, event.Method, rows[0].Method)
|
|
assert.Equal(t, eventQuery, rows[0].RawQuery)
|
|
assert.JSONEq(t, `{"archived":true}`, rows[0].Body)
|
|
}
|
|
|
|
// TestDeliverDatabase_ArchiveFailureFailsDelivery verifies that
|
|
// an archive error (here: an unparseable expiry in the target
|
|
// config) fails the delivery loudly: the attempt is recorded as
|
|
// failed with the error and the delivery is marked failed, not
|
|
// delivered.
|
|
func TestDeliverDatabase_ArchiveFailureFailsDelivery(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
env := setupArchiveTest(t)
|
|
tgt := env.seedDatabaseTarget(t, `{"expiry":"nonsense"}`)
|
|
|
|
webhookDB := testWebhookDB(t)
|
|
event := seedEvent(t, webhookDB, `{"archived":false}`)
|
|
d := seedDatabaseTargetDelivery(t, webhookDB, event, tgt)
|
|
|
|
env.eng.ExportDeliverDatabase(webhookDB, d)
|
|
|
|
var updated database.Delivery
|
|
|
|
require.NoError(t, webhookDB.First(
|
|
&updated, "id = ?", d.ID,
|
|
).Error)
|
|
assert.Equal(t,
|
|
database.DeliveryStatusFailed, updated.Status,
|
|
"archive failure must mark the delivery failed",
|
|
)
|
|
|
|
var results []database.DeliveryResult
|
|
|
|
require.NoError(t, webhookDB.Where(
|
|
"delivery_id = ?", d.ID,
|
|
).Find(&results).Error)
|
|
require.Len(t, results, 1)
|
|
assert.False(t,
|
|
results[0].Success,
|
|
"the attempt must be recorded as failed",
|
|
)
|
|
assert.Contains(t,
|
|
results[0].Error, "nonsense",
|
|
"the archive error must be recorded on the attempt",
|
|
)
|
|
|
|
assert.NoFileExists(t,
|
|
env.archivePath(tgt),
|
|
"no archive file should exist for a failed config",
|
|
)
|
|
}
|