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)
82 lines
2.4 KiB
Go
82 lines
2.4 KiB
Go
package storage_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/johannesboyne/gofakes3"
|
|
"github.com/johannesboyne/gofakes3/backend/s3mem"
|
|
|
|
"sneak.berlin/go/vaultik/internal/s3"
|
|
"sneak.berlin/go/vaultik/internal/storage"
|
|
)
|
|
|
|
// s3TestBucket is the bucket created for each in-process S3 server.
|
|
const s3TestBucket = "test-bucket"
|
|
|
|
// newS3Storer builds an s3:// backend backed by a fresh in-process
|
|
// S3 server. It reuses the same in-memory S3 harness (gofakes3 + s3mem
|
|
// over httptest) that internal/s3 and the not-found regression test use,
|
|
// so no new mock or dependency is introduced. Each call gets its own
|
|
// server, bucket, and client, so the conformance suite's per-section
|
|
// instances stay isolated.
|
|
//
|
|
//nolint:ireturn // conformance runs against the Storer interface by design
|
|
func newS3Storer(t *testing.T) storage.Storer {
|
|
t.Helper()
|
|
|
|
backend := s3mem.New()
|
|
|
|
err := backend.CreateBucket(s3TestBucket)
|
|
if err != nil {
|
|
t.Fatalf("create bucket: %v", err)
|
|
}
|
|
|
|
srv := httptest.NewServer(gofakes3.New(backend).Server())
|
|
t.Cleanup(srv.Close)
|
|
|
|
client, err := s3.NewClient(context.Background(), s3.Config{
|
|
Endpoint: srv.URL,
|
|
Bucket: s3TestBucket,
|
|
AccessKeyID: "test",
|
|
SecretAccessKey: "test",
|
|
Region: "us-east-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("new client: %v", err)
|
|
}
|
|
|
|
return storage.NewS3Storer(client)
|
|
}
|
|
|
|
// TestS3Storer runs the shared Storer contract against the s3:// backend,
|
|
// so it is held to the same round-trip, list, delete, and not-found
|
|
// behaviour as the file:// backend.
|
|
func TestS3Storer(t *testing.T) {
|
|
t.Parallel()
|
|
runStorerConformance(t, newS3Storer)
|
|
}
|
|
|
|
// TestS3StorerMissingKeyMapsToErrNotFound pins the specific contract that a
|
|
// missing object surfaces as storage.ErrNotFound rather than the raw AWS SDK
|
|
// error. Without the mapping, errors.Is(err, storage.ErrNotFound) is false on
|
|
// s3 and callers would branch differently per backend.
|
|
func TestS3StorerMissingKeyMapsToErrNotFound(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
storer := newS3Storer(t)
|
|
ctx := context.Background()
|
|
|
|
_, err := storer.Get(ctx, "does-not-exist")
|
|
if !errors.Is(err, storage.ErrNotFound) {
|
|
t.Errorf("Get on missing key: got %v, want ErrNotFound", err)
|
|
}
|
|
|
|
_, err = storer.Stat(ctx, "does-not-exist")
|
|
if !errors.Is(err, storage.ErrNotFound) {
|
|
t.Errorf("Stat on missing key: got %v, want ErrNotFound", err)
|
|
}
|
|
}
|