check / check (pull_request) Failing after 1s
internal/storage had no tests. This adds table-driven ParseStorageURL coverage and a shared Storer conformance suite, defined once in conformance_test.go and run against every backend that can run in-process: file:// over a temp dir and s3:// over the same in-memory S3 harness (gofakes3 + s3mem) that internal/s3 and the not-found test use, so a new backend inherits the contract by passing its constructor. The rclone backend gets construction and argument-shaping tests via rclone's in-process ":local" backend, plus the missing-remote error path; a comment records that its data-plane operations need a configured remote and so cannot run in a unit test. Tests only; no production code changed and no defect surfaced. Model: opus-4-8
199 lines
4.8 KiB
Go
199 lines
4.8 KiB
Go
package storage_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/vaultik/internal/storage"
|
|
)
|
|
|
|
// runStorerConformance is the shared Storer contract. Every backend that
|
|
// can run in-process is expected to pass it: TestFileStorer runs it against
|
|
// file://, TestS3Storer against s3://. A new backend inherits this coverage
|
|
// by passing its own constructor, so the contract is defined once.
|
|
//
|
|
// It exercises the public Storer interface: round-trip, stat, list with
|
|
// prefix filtering, overwrite, delete, delete-of-missing, and not-found on
|
|
// Get and Stat. Each section takes its own fresh backend instance, so the
|
|
// order of sections never matters and no section sees another's objects.
|
|
func runStorerConformance(t *testing.T, newStorer func(*testing.T) storage.Storer) {
|
|
t.Helper()
|
|
|
|
conformanceRoundTrip(t, newStorer(t))
|
|
conformanceOverwrite(t, newStorer(t))
|
|
conformanceList(t, newStorer(t))
|
|
conformanceDelete(t, newStorer(t))
|
|
conformanceNotFound(t, newStorer(t))
|
|
}
|
|
|
|
// conformanceRoundTrip stores a nested key, then reads it back and stats it.
|
|
func conformanceRoundTrip(t *testing.T, s storage.Storer) {
|
|
t.Helper()
|
|
|
|
ctx := context.Background()
|
|
key := "blobs/aa/bb/object.bin"
|
|
want := []byte("round-trip payload")
|
|
|
|
err := s.Put(ctx, key, bytes.NewReader(want))
|
|
if err != nil {
|
|
t.Fatalf("Put: %v", err)
|
|
}
|
|
|
|
got := getBytes(t, s, key)
|
|
if !bytes.Equal(got, want) {
|
|
t.Errorf("Get returned %q, want %q", got, want)
|
|
}
|
|
|
|
info, err := s.Stat(ctx, key)
|
|
if err != nil {
|
|
t.Fatalf("Stat: %v", err)
|
|
}
|
|
|
|
if info.Key != key {
|
|
t.Errorf("Stat key = %q, want %q", info.Key, key)
|
|
}
|
|
|
|
if info.Size != int64(len(want)) {
|
|
t.Errorf("Stat size = %d, want %d", info.Size, len(want))
|
|
}
|
|
}
|
|
|
|
// conformanceOverwrite checks that a second Put replaces the first.
|
|
func conformanceOverwrite(t *testing.T, s storage.Storer) {
|
|
t.Helper()
|
|
|
|
ctx := context.Background()
|
|
key := "meta/snapshot.json"
|
|
|
|
err := s.Put(ctx, key, bytes.NewReader([]byte("first")))
|
|
if err != nil {
|
|
t.Fatalf("first Put: %v", err)
|
|
}
|
|
|
|
want := []byte("second and longer payload")
|
|
|
|
err = s.Put(ctx, key, bytes.NewReader(want))
|
|
if err != nil {
|
|
t.Fatalf("second Put: %v", err)
|
|
}
|
|
|
|
got := getBytes(t, s, key)
|
|
if !bytes.Equal(got, want) {
|
|
t.Errorf("after overwrite Get returned %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// conformanceList checks prefix filtering and the empty result for a
|
|
// prefix that matches nothing.
|
|
func conformanceList(t *testing.T, s storage.Storer) {
|
|
t.Helper()
|
|
|
|
ctx := context.Background()
|
|
keys := []string{"blobs/aa/one", "blobs/bb/two", "meta/three"}
|
|
|
|
for _, k := range keys {
|
|
err := s.Put(ctx, k, bytes.NewReader([]byte("data")))
|
|
if err != nil {
|
|
t.Fatalf("Put %q: %v", k, err)
|
|
}
|
|
}
|
|
|
|
if got := listSorted(t, s, ""); !reflect.DeepEqual(got, keys) {
|
|
t.Errorf("List(\"\") = %v, want %v", got, keys)
|
|
}
|
|
|
|
wantBlobs := []string{"blobs/aa/one", "blobs/bb/two"}
|
|
if got := listSorted(t, s, "blobs/"); !reflect.DeepEqual(got, wantBlobs) {
|
|
t.Errorf("List(\"blobs/\") = %v, want %v", got, wantBlobs)
|
|
}
|
|
|
|
if got := listSorted(t, s, "absent/"); len(got) != 0 {
|
|
t.Errorf("List(\"absent/\") = %v, want empty", got)
|
|
}
|
|
}
|
|
|
|
// conformanceDelete checks that Delete removes an object and that deleting
|
|
// a missing key is not an error.
|
|
func conformanceDelete(t *testing.T, s storage.Storer) {
|
|
t.Helper()
|
|
|
|
ctx := context.Background()
|
|
key := "blobs/cc/gone.bin"
|
|
|
|
err := s.Put(ctx, key, bytes.NewReader([]byte("temporary")))
|
|
if err != nil {
|
|
t.Fatalf("Put: %v", err)
|
|
}
|
|
|
|
err = s.Delete(ctx, key)
|
|
if err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
|
|
_, err = s.Get(ctx, key)
|
|
if !errors.Is(err, storage.ErrNotFound) {
|
|
t.Errorf("Get after Delete error = %v, want ErrNotFound", err)
|
|
}
|
|
|
|
err = s.Delete(ctx, key)
|
|
if err != nil {
|
|
t.Errorf("Delete of missing key = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
// conformanceNotFound checks Get and Stat on an absent key.
|
|
func conformanceNotFound(t *testing.T, s storage.Storer) {
|
|
t.Helper()
|
|
|
|
ctx := context.Background()
|
|
key := "never/written"
|
|
|
|
_, err := s.Get(ctx, key)
|
|
if !errors.Is(err, storage.ErrNotFound) {
|
|
t.Errorf("Get error = %v, want ErrNotFound", err)
|
|
}
|
|
|
|
_, err = s.Stat(ctx, key)
|
|
if !errors.Is(err, storage.ErrNotFound) {
|
|
t.Errorf("Stat error = %v, want ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
// getBytes reads a key fully and closes the reader.
|
|
func getBytes(t *testing.T, s storage.Storer, key string) []byte {
|
|
t.Helper()
|
|
|
|
rc, err := s.Get(context.Background(), key)
|
|
if err != nil {
|
|
t.Fatalf("Get %q: %v", key, err)
|
|
}
|
|
|
|
defer func() { _ = rc.Close() }()
|
|
|
|
data, err := io.ReadAll(rc)
|
|
if err != nil {
|
|
t.Fatalf("read %q: %v", key, err)
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
// listSorted returns the keys under a prefix in a stable order.
|
|
func listSorted(t *testing.T, s storage.Storer, prefix string) []string {
|
|
t.Helper()
|
|
|
|
keys, err := s.List(context.Background(), prefix)
|
|
if err != nil {
|
|
t.Fatalf("List %q: %v", prefix, err)
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
return keys
|
|
}
|