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: each name is lowercased, keeps ASCII letters and digits, turns every other run of characters into one dash, and is cut to 40 characters. 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. A rename never replaces a file: if one already has the new name, the edit is refused. 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
638 lines
16 KiB
Go
638 lines
16 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())
|
|
}
|