check / check (push) Waiting to run
A snapshot ID is hostname_name_timestamp, and purge took the name to be everything between the first and the last underscore. With a hostname such as my_host the name home came out as host_home, so `snapshot purge --keep-latest --snapshot home` found nothing to delete and `snapshot create --prune` purged nothing without a message. The name is now read by removing the hostname stored with the snapshot, in the short form the ID uses, so both may contain underscores. This was chosen over rejecting underscores in `hostname` when the config loads, which would also stop restores on such a host. The purge consistency test stored a hostname that did not match its snapshot IDs; it now matches, as it always does in production. Model: opus-5-5
182 lines
4.3 KiB
Go
182 lines
4.3 KiB
Go
package vaultik //nolint:testpackage // exercises unexported parse helpers
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestParseSnapshotName(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
snapshotID string
|
|
hostname string
|
|
want string
|
|
}{
|
|
{
|
|
name: "standard format with name",
|
|
snapshotID: "myhost_home_2026-01-12T14:41:15Z",
|
|
hostname: "myhost",
|
|
want: "home",
|
|
},
|
|
{
|
|
name: "standard format with different name",
|
|
snapshotID: "server1_system_2026-02-15T09:30:00Z",
|
|
hostname: "server1",
|
|
want: "system",
|
|
},
|
|
{
|
|
name: "name with underscores",
|
|
snapshotID: "myhost_my_special_backup_2026-03-01T00:00:00Z",
|
|
hostname: "myhost",
|
|
want: "my_special_backup",
|
|
},
|
|
{
|
|
name: "hostname with underscores",
|
|
snapshotID: "my_host_docs_2026-03-01T00:00:00Z",
|
|
hostname: "my_host",
|
|
want: "docs",
|
|
},
|
|
{
|
|
name: "stored hostname with domain",
|
|
snapshotID: "my_host_mail_2026-03-01T00:00:00Z",
|
|
hostname: "my_host.example.com",
|
|
want: "mail",
|
|
},
|
|
{
|
|
name: "no name",
|
|
snapshotID: "my_host_2026-03-01T00:00:00Z",
|
|
hostname: "my_host",
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := parseSnapshotName(tt.snapshotID, tt.hostname)
|
|
if got != tt.want {
|
|
t.Errorf("parseSnapshotName(%q, %q) = %q, want %q",
|
|
tt.snapshotID, tt.hostname, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseDuration(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
input string
|
|
want time.Duration
|
|
err bool
|
|
}{
|
|
// Go units, including the m-is-minutes / mo-is-months distinction
|
|
// that this parser exists to keep straight.
|
|
{"10ns", 10 * time.Nanosecond, false},
|
|
{"10us", 10 * time.Microsecond, false},
|
|
{"500ms", 500 * time.Millisecond, false},
|
|
{"30s", 30 * time.Second, false},
|
|
{"6m", 6 * time.Minute, false},
|
|
{"1h", time.Hour, false},
|
|
{"1.5h", 90 * time.Minute, false},
|
|
// Extended calendar units.
|
|
{"30d", 30 * 24 * time.Hour, false},
|
|
{"3days", 3 * 24 * time.Hour, false},
|
|
{"4w", 4 * 7 * 24 * time.Hour, false},
|
|
{"2weeks", 2 * 7 * 24 * time.Hour, false},
|
|
{"6mo", 180 * 24 * time.Hour, false},
|
|
{"1month", 30 * 24 * time.Hour, false},
|
|
{"1y", 365 * 24 * time.Hour, false},
|
|
{"2years", 2 * 365 * 24 * time.Hour, false},
|
|
// Combined units.
|
|
{"2w3d", 2*7*24*time.Hour + 3*24*time.Hour, false},
|
|
{"1y6mo", 365*24*time.Hour + 180*24*time.Hour, false},
|
|
// Rejected inputs.
|
|
{"6", 0, true}, // bare number, no unit
|
|
{"0", 0, true}, // bare number; time.ParseDuration accepts it
|
|
{"+0", 0, true}, // bare number; time.ParseDuration accepts it
|
|
{"5x", 0, true}, // unknown unit
|
|
{"-5d", 0, true}, // negative, extended unit
|
|
{"-5h", 0, true}, // negative, Go unit
|
|
{"", 0, true}, // empty
|
|
{"garbage", 0, true},
|
|
// Characters outside the number-and-unit parts must not be
|
|
// skipped: 1.0y read as 0y would select every snapshot.
|
|
{"1.0y", 0, true},
|
|
{"2.1w", 0, true},
|
|
{"1,5y", 0, true},
|
|
{"30 days", 0, true},
|
|
{"30 days ago", 0, true},
|
|
{"x7d", 0, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, err := parseDuration(tt.input)
|
|
if tt.err {
|
|
if err == nil {
|
|
t.Fatalf("expected error for %q, got %v", tt.input, got)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error for %q: %v", tt.input, err)
|
|
}
|
|
|
|
if got != tt.want {
|
|
t.Errorf("parseDuration(%q) = %v, want %v", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseSnapshotTimestamp(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
snapshotID string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid with name",
|
|
snapshotID: "myhost_home_2026-01-12T14:41:15Z",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid without name",
|
|
snapshotID: "myhost_2026-01-12T14:41:15Z",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid - single part",
|
|
snapshotID: "nounderscore",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid - bad timestamp",
|
|
snapshotID: "myhost_home_notadate",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := parseSnapshotTimestamp(tt.snapshotID)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("parseSnapshotTimestamp(%q) error = %v, wantErr %v",
|
|
tt.snapshotID, err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|