check / check (push) Waiting to run
Each database target now has its own archive file, archive-WEBHOOKNAME-TARGETNAME-TARGETID.db, instead of one archive-WEBHOOKID.db per webhook. delivery.ArchiveFileName builds the name. A change of webhook or target name renames its archive files under the archive writer's lock, before the new name is saved, and back again if the save fails. Webhook edits, target edits and target creation run one at a time, so no two of them interleave. A rename never replaces a file, and one that fails part way moves back what it moved. Deleting a target evicts only that target's writer. Archive files are never deleted, and nothing looks for files under the old name. Model: opus-5-5
670 lines
17 KiB
Go
670 lines
17 KiB
Go
package delivery_test
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
_ "modernc.org/sqlite" // Pure Go SQLite driver.
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/delivery"
|
|
)
|
|
|
|
func archiveTestLogger() *slog.Logger {
|
|
return slog.New(slog.NewTextHandler(
|
|
os.Stderr,
|
|
&slog.HandlerOptions{Level: slog.LevelDebug},
|
|
))
|
|
}
|
|
|
|
// openArchiveDBForRead opens an archive file read-only so a
|
|
// test can inspect the rows the writer persisted.
|
|
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{},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
return gdb
|
|
}
|
|
|
|
// archiveFileSuffixes returns the archive file itself and the
|
|
// SQLite sidecars that accompany an open database. A test that
|
|
// asserts no archive was created has to check all of them.
|
|
func archiveFileSuffixes() []string {
|
|
return []string{"", "-wal", "-shm"}
|
|
}
|
|
|
|
// removeArchiveFiles simulates an operator moving the archive
|
|
// away by deleting the SQLite file and its sidecar files.
|
|
func removeArchiveFiles(t *testing.T, path string) {
|
|
t.Helper()
|
|
|
|
for _, suffix := range []string{
|
|
"", "-wal", "-shm", "-journal",
|
|
} {
|
|
err := os.Remove(path + suffix)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
t.Fatalf("removing %s%s: %v", path, suffix, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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}`)
|
|
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 []delivery.ExportArchivedEvent
|
|
|
|
require.NoError(t, rdb.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.JSONEq(t, `{"archived":true}`, rows[0].Body)
|
|
}
|
|
|
|
func TestArchiveWriter_WritesRow(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
path := filepath.Join(t.TempDir(), "archive-wh.db")
|
|
w := delivery.NewExportArchiveWriter(
|
|
path, archiveTestLogger(), 0,
|
|
)
|
|
|
|
row := delivery.ExportArchivedEvent{
|
|
EventID: "ev-1",
|
|
WebhookID: "wh-1",
|
|
EntrypointID: "ep-1",
|
|
Method: "POST",
|
|
Headers: `{"X":"Y"}`,
|
|
Body: `{"hello":"world"}`,
|
|
ContentType: "application/json",
|
|
}
|
|
|
|
require.NoError(t, w.Write(row, 0))
|
|
assert.FileExists(t, path)
|
|
|
|
var got []delivery.ExportArchivedEvent
|
|
|
|
require.NoError(t, w.DB().Find(&got).Error)
|
|
require.Len(t, got, 1)
|
|
assert.Equal(t, "ev-1", got[0].EventID)
|
|
assert.Equal(t, "wh-1", got[0].WebhookID)
|
|
assert.Equal(t, "ep-1", got[0].EntrypointID)
|
|
assert.Equal(t, row.Method, got[0].Method)
|
|
assert.Equal(t, row.ContentType, got[0].ContentType)
|
|
assert.JSONEq(t, `{"hello":"world"}`, got[0].Body)
|
|
assert.False(t, got[0].ArchivedAt.IsZero())
|
|
}
|
|
|
|
func TestArchiveWriter_RecreatesAfterRemoval(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
path := filepath.Join(t.TempDir(), "archive-wh.db")
|
|
w := delivery.NewExportArchiveWriter(
|
|
path, archiveTestLogger(), 0,
|
|
)
|
|
|
|
require.NoError(t, w.Write(
|
|
delivery.ExportArchivedEvent{EventID: "a"}, 0,
|
|
))
|
|
assert.FileExists(t, path)
|
|
|
|
// The operator moves the archive away while the handle is
|
|
// still open.
|
|
removeArchiveFiles(t, path)
|
|
require.NoFileExists(t, path)
|
|
|
|
// The next write recreates the file with a fresh schema and
|
|
// only the new row.
|
|
require.NoError(t, w.Write(
|
|
delivery.ExportArchivedEvent{EventID: "b"}, 0,
|
|
))
|
|
assert.FileExists(t, path)
|
|
|
|
var got []delivery.ExportArchivedEvent
|
|
|
|
require.NoError(t, w.DB().Find(&got).Error)
|
|
require.Len(t, got, 1)
|
|
assert.Equal(t, "b", got[0].EventID)
|
|
}
|
|
|
|
func TestArchiveWriter_ReopenDebounce(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// A generous debounce keeps the two rapid writes inside
|
|
// the window even on a heavily loaded test machine.
|
|
path := filepath.Join(t.TempDir(), "archive-wh.db")
|
|
w := delivery.NewExportArchiveWriter(
|
|
path, archiveTestLogger(), 2*time.Second,
|
|
)
|
|
|
|
require.NoError(t, w.Write(
|
|
delivery.ExportArchivedEvent{EventID: "a"}, 0,
|
|
))
|
|
require.NoError(t, w.Write(
|
|
delivery.ExportArchivedEvent{EventID: "b"}, 0,
|
|
))
|
|
|
|
// Two writes inside the debounce window trigger only the
|
|
// initial open — no extra close/reopen.
|
|
assert.Equal(t, 1, w.Reopens())
|
|
|
|
time.Sleep(2100 * time.Millisecond)
|
|
|
|
require.NoError(t, w.Write(
|
|
delivery.ExportArchivedEvent{EventID: "c"}, 0,
|
|
))
|
|
|
|
// A write after the window elapses closes and reopens once.
|
|
assert.Equal(t, 2, w.Reopens())
|
|
}
|
|
|
|
func TestArchiveWriter_ExpiryPrune(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
path := filepath.Join(t.TempDir(), "archive-wh.db")
|
|
w := delivery.NewExportArchiveWriter(
|
|
path, archiveTestLogger(), 0,
|
|
)
|
|
|
|
require.NoError(t, w.Open(0))
|
|
|
|
old := delivery.ExportArchivedEvent{
|
|
EventID: "old",
|
|
ArchivedAt: time.Now().Add(-2 * time.Hour),
|
|
}
|
|
fresh := delivery.ExportArchivedEvent{
|
|
EventID: "fresh",
|
|
ArchivedAt: time.Now(),
|
|
}
|
|
|
|
require.NoError(t, w.DB().Create(&old).Error)
|
|
require.NoError(t, w.DB().Create(&fresh).Error)
|
|
|
|
// Reopening with a one-hour expiry prunes the old row.
|
|
require.NoError(t, w.Reopen(time.Hour))
|
|
|
|
var got []delivery.ExportArchivedEvent
|
|
|
|
require.NoError(t, w.DB().Find(&got).Error)
|
|
require.Len(t, got, 1)
|
|
assert.Equal(t, "fresh", got[0].EventID)
|
|
}
|
|
|
|
func TestParseArchiveExpiry(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
want time.Duration
|
|
wantErr bool
|
|
}{
|
|
{"empty config", "", 0, false},
|
|
{"explicit never", `{"expiry":"never"}`, 0, false},
|
|
{"empty expiry", `{"expiry":""}`, 0, false},
|
|
{"duration", `{"expiry":"1h"}`, time.Hour, false},
|
|
{"unparseable", `{"expiry":"nonsense"}`, 0, true},
|
|
{"zero duration", `{"expiry":"0s"}`, 0, true},
|
|
{"negative duration", `{"expiry":"-5h"}`, 0, true},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, err := delivery.ExportParseArchiveExpiry(tc.in)
|
|
if tc.wantErr {
|
|
require.Error(t, err)
|
|
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tc.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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",
|
|
)
|
|
}
|
|
|
|
func TestValidateArchiveExpiry(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
valid := []string{"", "never", "1h", "720h", "30m"}
|
|
for _, in := range valid {
|
|
require.NoError(t,
|
|
delivery.ValidateArchiveExpiry(in),
|
|
"expiry %q should be accepted", in,
|
|
)
|
|
}
|
|
|
|
invalid := []string{"nonsense", "7d", "-5h", "0s", "0"}
|
|
for _, in := range invalid {
|
|
require.Error(t,
|
|
delivery.ValidateArchiveExpiry(in),
|
|
"expiry %q should be rejected", in,
|
|
)
|
|
}
|
|
}
|
|
|
|
// TestArchiveFileName pins the archive file name and the rules
|
|
// that make a webhook or target name safe to put in it.
|
|
func TestArchiveFileName(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const id = "3f2a1c9e-8d4b-4c1a-9e2f-0a1b2c3d4e5f"
|
|
|
|
cases := []struct {
|
|
name string
|
|
webhook string
|
|
target string
|
|
want string
|
|
}{
|
|
{
|
|
"plain names", "orders", "archive",
|
|
"archive-orders-archive-" + id + ".db",
|
|
},
|
|
{
|
|
"lowercased", "Orders", "Main Archive",
|
|
"archive-orders-main-archive-" + id + ".db",
|
|
},
|
|
{
|
|
"a run of other characters is one dash",
|
|
`a /\..b`, "c__--d",
|
|
"archive-a-b-c-d-" + id + ".db",
|
|
},
|
|
{
|
|
"no dash at either end", " --orders!! ", "(archive)",
|
|
"archive-orders-archive-" + id + ".db",
|
|
},
|
|
{
|
|
"path separators", "../../etc/passwd", "a/b",
|
|
"archive-etc-passwd-a-b-" + id + ".db",
|
|
},
|
|
{
|
|
"letters outside ASCII are dropped",
|
|
"Bestellungen Größe", "café",
|
|
"archive-bestellungen-gr-e-caf-" + id + ".db",
|
|
},
|
|
{
|
|
"nothing left is unnamed", "", "!!!",
|
|
"archive-unnamed-unnamed-" + id + ".db",
|
|
},
|
|
{
|
|
"cut to 40 characters", strings.Repeat("a", 50), "x",
|
|
"archive-" + strings.Repeat("a", 40) + "-x-" + id + ".db",
|
|
},
|
|
{
|
|
"no dash left by the cut",
|
|
strings.Repeat("a", 39) + " b", "x",
|
|
"archive-" + strings.Repeat("a", 39) + "-x-" + id + ".db",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
assert.Equal(
|
|
t, tc.want,
|
|
delivery.ArchiveFileName(tc.webhook, tc.target, id),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDeliverDatabase_EachTargetHasItsOwnArchive proves two
|
|
// database targets of one webhook archive into separate files.
|
|
func TestDeliverDatabase_EachTargetHasItsOwnArchive(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupArchiveTest(t)
|
|
first := env.seedDatabaseTarget(t, "")
|
|
second := env.addDatabaseTarget(t, first.WebhookID, "")
|
|
|
|
webhookDB := testWebhookDB(t)
|
|
event := seedEvent(t, webhookDB, `{"n":1}`)
|
|
|
|
for _, tgt := range []*database.Target{first, second} {
|
|
env.eng.ExportDeliverDatabase(
|
|
webhookDB,
|
|
seedDatabaseTargetDelivery(t, webhookDB, event, tgt),
|
|
)
|
|
}
|
|
|
|
require.NotEqual(
|
|
t, env.archivePath(first), env.archivePath(second),
|
|
)
|
|
assert.Equal(
|
|
t, []string{event.ID},
|
|
archivedEventIDs(t, env.archivePath(first)),
|
|
)
|
|
assert.Equal(
|
|
t, []string{event.ID},
|
|
archivedEventIDs(t, env.archivePath(second)),
|
|
)
|
|
}
|
|
|
|
// TestRename_MovesTheFile proves a rename moves the archive, rows
|
|
// and all, and that later writes go to the new name.
|
|
func TestRename_MovesTheFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupArchiveTest(t)
|
|
tgt := env.seedDatabaseTarget(t, "")
|
|
oldPath := env.archivePath(tgt)
|
|
|
|
webhookDB := testWebhookDB(t)
|
|
first := seedEvent(t, webhookDB, `{"n":1}`)
|
|
env.eng.ExportDeliverDatabase(
|
|
webhookDB, seedDatabaseTargetDelivery(t, webhookDB, first, tgt),
|
|
)
|
|
require.FileExists(t, oldPath)
|
|
|
|
require.NoError(
|
|
t, env.eng.Rename(tgt.ID, "Orders", "Long Term"),
|
|
)
|
|
|
|
newPath := filepath.Join(
|
|
env.dataDir, "archive-orders-long-term-"+tgt.ID+".db",
|
|
)
|
|
|
|
assert.NoFileExists(t, oldPath)
|
|
assert.Equal(t, []string{first.ID}, archivedEventIDs(t, newPath))
|
|
|
|
second := seedEvent(t, webhookDB, `{"n":2}`)
|
|
env.eng.ExportDeliverDatabase(
|
|
webhookDB,
|
|
seedDatabaseTargetDelivery(t, webhookDB, second, tgt),
|
|
)
|
|
|
|
assert.ElementsMatch(
|
|
t, []string{first.ID, second.ID},
|
|
archivedEventIDs(t, newPath),
|
|
)
|
|
assert.NoFileExists(
|
|
t, oldPath, "a write after the rename must use the new name",
|
|
)
|
|
}
|
|
|
|
// TestRename_NeverReplacesAFile plants a file at the new name, once
|
|
// the .db alone, once a lone -wal and once a lone -shm, and proves
|
|
// each time that the rename is refused, the planted file survives,
|
|
// and the archive keeps its name and its rows.
|
|
func TestRename_NeverReplacesAFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, suffix := range archiveFileSuffixes() {
|
|
t.Run("planted .db"+suffix, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupArchiveTest(t)
|
|
tgt := env.seedDatabaseTarget(t, "")
|
|
oldPath := env.archivePath(tgt)
|
|
|
|
webhookDB := testWebhookDB(t)
|
|
first := seedEvent(t, webhookDB, `{"n":1}`)
|
|
env.eng.ExportDeliverDatabase(
|
|
webhookDB,
|
|
seedDatabaseTargetDelivery(t, webhookDB, first, tgt),
|
|
)
|
|
|
|
newPath := filepath.Join(
|
|
env.dataDir, "archive-orders-long-term-"+tgt.ID+".db",
|
|
)
|
|
plantedPath := newPath + suffix
|
|
require.NoError(
|
|
t, os.WriteFile(plantedPath, []byte("planted"), 0o600),
|
|
)
|
|
|
|
require.ErrorIs(
|
|
t, env.eng.Rename(tgt.ID, "Orders", "Long Term"),
|
|
delivery.ErrArchiveNameTaken,
|
|
)
|
|
|
|
//nolint:gosec // reads the file the test planted under t.TempDir()
|
|
planted, err := os.ReadFile(plantedPath)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "planted", string(planted))
|
|
|
|
second := seedEvent(t, webhookDB, `{"n":2}`)
|
|
env.eng.ExportDeliverDatabase(
|
|
webhookDB,
|
|
seedDatabaseTargetDelivery(t, webhookDB, second, tgt),
|
|
)
|
|
|
|
assert.ElementsMatch(
|
|
t, []string{first.ID, second.ID},
|
|
archivedEventIDs(t, oldPath),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRename_BeforeTheNameIsSaved covers the order the handlers
|
|
// use: they rename before they save the new name, so a delivery in
|
|
// between must write under the new name although the main database
|
|
// still has the old one. It also shows that renaming an archive that
|
|
// does not exist yet is not an error.
|
|
func TestRename_BeforeTheNameIsSaved(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := setupArchiveTest(t)
|
|
tgt := env.seedDatabaseTarget(t, "")
|
|
|
|
require.NoError(
|
|
t, env.eng.Rename(tgt.ID, "Orders", "Archive"),
|
|
)
|
|
|
|
webhookDB := testWebhookDB(t)
|
|
event := seedEvent(t, webhookDB, `{"n":1}`)
|
|
env.eng.ExportDeliverDatabase(
|
|
webhookDB, seedDatabaseTargetDelivery(t, webhookDB, event, tgt),
|
|
)
|
|
|
|
assert.FileExists(
|
|
t,
|
|
filepath.Join(
|
|
env.dataDir, "archive-orders-archive-"+tgt.ID+".db",
|
|
),
|
|
)
|
|
assert.NoFileExists(t, env.archivePath(tgt))
|
|
}
|
|
|
|
// TestArchiveWriter_RenameMovesSidecars proves a rename carries
|
|
// the -wal and -shm a crash can leave beside an archive no handle
|
|
// has opened since. SQLite finds them by name, so a -wal left
|
|
// behind would lose the transactions it holds.
|
|
func TestArchiveWriter_RenameMovesSidecars(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
oldPath := filepath.Join(dir, "archive-old.db")
|
|
newPath := filepath.Join(dir, "archive-new.db")
|
|
|
|
for _, suffix := range archiveFileSuffixes() {
|
|
require.NoError(
|
|
t, os.WriteFile(oldPath+suffix, []byte(suffix), 0o600),
|
|
)
|
|
}
|
|
|
|
w := delivery.NewExportArchiveWriter(
|
|
oldPath, archiveTestLogger(), 0,
|
|
)
|
|
|
|
require.NoError(t, w.Rename("archive-new.db"))
|
|
|
|
for _, suffix := range archiveFileSuffixes() {
|
|
assert.NoFileExists(t, oldPath+suffix)
|
|
assert.FileExists(t, newPath+suffix)
|
|
}
|
|
|
|
assert.Equal(t, newPath, w.Path())
|
|
}
|
|
|
|
// TestArchiveWriter_RenameMovesBackOnFailure makes the -wal fail to
|
|
// move after the .db has moved, and proves the .db is moved back, so
|
|
// the archive is never split across two names. The new name is 255
|
|
// bytes, the longest a file name may be, so the .db can take it but
|
|
// the -wal, four bytes longer, cannot.
|
|
func TestArchiveWriter_RenameMovesBackOnFailure(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
oldPath := filepath.Join(dir, "archive-old.db")
|
|
newName := strings.Repeat("a", 252) + ".db"
|
|
|
|
for _, suffix := range archiveFileSuffixes() {
|
|
require.NoError(
|
|
t, os.WriteFile(oldPath+suffix, []byte(suffix), 0o600),
|
|
)
|
|
}
|
|
|
|
w := delivery.NewExportArchiveWriter(
|
|
oldPath, archiveTestLogger(), 0,
|
|
)
|
|
|
|
require.Error(t, w.Rename(newName))
|
|
|
|
for _, suffix := range archiveFileSuffixes() {
|
|
assert.FileExists(t, oldPath+suffix)
|
|
}
|
|
|
|
assert.NoFileExists(t, filepath.Join(dir, newName))
|
|
assert.Equal(t, oldPath, w.Path())
|
|
}
|