internal/storage, the package that parses store URLs and selects the backend, had no tests. Adds table-driven tests for URL parsing (each scheme, query parameters, malformed input, unknown scheme, backend type chosen); one shared conformance suite for the Storer interface, run against the file backend in a temp directory and the s3 backend on the in-process harness internal/s3 already uses, so a new backend inherits it; and rclone construction and argument tests using its in-process local backend. A comment records that rclone data operations need a configured remote and are not unit-tested. No production code changed and no defect surfaced. model: claude-opus-4-8 (implementation, review); claude-fable-5-1 (merge)
111 lines
2.8 KiB
Go
111 lines
2.8 KiB
Go
package storage_test
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/vaultik/internal/storage"
|
|
)
|
|
|
|
// TestParseStorageURLValid checks that each supported scheme parses into
|
|
// the expected fields, since those fields decide which backend is built.
|
|
func TestParseStorageURLValid(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const bucket = "mybucket"
|
|
|
|
cases := []struct {
|
|
name string
|
|
raw string
|
|
want *storage.URL
|
|
}{
|
|
{
|
|
name: "file absolute path",
|
|
raw: "file:///var/backups/vaultik",
|
|
want: &storage.URL{Scheme: "file", Prefix: "/var/backups/vaultik"},
|
|
},
|
|
{
|
|
name: "s3 bucket and prefix, ssl defaults on",
|
|
raw: "s3://mybucket/backups/host",
|
|
want: &storage.URL{
|
|
Scheme: "s3", Bucket: bucket,
|
|
Prefix: "backups/host", UseSSL: true,
|
|
},
|
|
},
|
|
{
|
|
name: "s3 bucket only",
|
|
raw: "s3://mybucket",
|
|
want: &storage.URL{Scheme: "s3", Bucket: bucket, UseSSL: true},
|
|
},
|
|
{
|
|
name: "s3 with endpoint, region, ssl off",
|
|
raw: "s3://mybucket?endpoint=minio.example.com®ion=us-west-2&ssl=false",
|
|
want: &storage.URL{
|
|
Scheme: "s3", Bucket: bucket,
|
|
Endpoint: "minio.example.com", Region: "us-west-2", UseSSL: false,
|
|
},
|
|
},
|
|
{
|
|
name: "rclone remote and path",
|
|
raw: "rclone://gdrive/backups/host",
|
|
want: &storage.URL{
|
|
Scheme: "rclone", RcloneRemote: "gdrive", Prefix: "backups/host",
|
|
},
|
|
},
|
|
{
|
|
name: "rclone remote only",
|
|
raw: "rclone://gdrive",
|
|
want: &storage.URL{Scheme: "rclone", RcloneRemote: "gdrive"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, err := storage.ParseStorageURL(tc.raw)
|
|
if err != nil {
|
|
t.Fatalf("ParseStorageURL(%q) returned error: %v", tc.raw, err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, tc.want) {
|
|
t.Errorf("ParseStorageURL(%q) = %+v, want %+v", tc.raw, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestParseStorageURLErrors checks that empty, missing, and unknown-scheme
|
|
// inputs fail with the documented sentinel errors instead of parsing to a
|
|
// wrong destination.
|
|
func TestParseStorageURLErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
raw string
|
|
wantErr error
|
|
}{
|
|
{"empty url", "", storage.ErrEmptyStorageURL},
|
|
{"file empty path", "file://", storage.ErrEmptyFilePath},
|
|
{"s3 missing bucket", "s3://", storage.ErrMissingBucket},
|
|
{"s3 missing bucket with path", "s3:///justprefix", storage.ErrMissingBucket},
|
|
{"rclone missing remote", "rclone://", storage.ErrMissingRemote},
|
|
{"unknown scheme", "gs://bucket/x", storage.ErrUnsupportedScheme},
|
|
{"no scheme", "/local/path", storage.ErrUnsupportedScheme},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := storage.ParseStorageURL(tc.raw)
|
|
if !errors.Is(err, tc.wantErr) {
|
|
t.Errorf("ParseStorageURL(%q) error = %v, want %v",
|
|
tc.raw, err, tc.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|