//go:build integration package integration_test import ( "bytes" "crypto/tls" "errors" "fmt" "net/http" "testing" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" testpredastore "github.com/mulgadc/spinifex/tests/fixtures/predastore" "github.com/stretchr/testify/require" ) // TestPredastoreObjectLifecycle ports // tests/e2e/single/predastore_test.go's runPredastoreObjectLifecycle: it // verifies predastore's user-visible object contract end to end — an object // is readable and intact after PutObject, absent after DeleteObject (both // GET and ListObjectsV2 stop returning it), and the store keeps serving // fresh writes once the delete has landed — against the real, in-process // predastore daemon (testpredastore.Start), not a tofu-provisioned cluster. // Not t.Parallel(): testpredastore.Start calls t.Setenv on whichever test // first starts the shared fixture daemon in this binary, and t.Setenv panics // if combined with t.Parallel on that same test. func TestPredastoreObjectLifecycle(t *testing.T) { fixture := testpredastore.Start(t) cli := predastoreS3Client(t, fixture) bucket := fmt.Sprintf("predastore-lifecycle-%d", time.Now().UnixNano()) _, err := cli.CreateBucket(&s3.CreateBucketInput{Bucket: aws.String(bucket)}) require.NoError(t, err, "create bucket %s", bucket) t.Cleanup(func() { _, _ = cli.DeleteBucket(&s3.DeleteBucketInput{Bucket: aws.String(bucket)}) }) const key = "lifecycle/object" payload := bytes.Repeat([]byte("p"), 256<<10) // 256 KiB // Write, then confirm the object is readable and byte-identical. _, err = cli.PutObject(&s3.PutObjectInput{ Bucket: aws.String(bucket), Key: aws.String(key), Body: bytes.NewReader(payload), }) require.NoError(t, err, "put %s", key) got, err := predastoreGetBytes(t, cli, bucket, key) require.NoError(t, err, "get %s after put", key) require.True(t, bytes.Equal(got, payload), "get %s after put returned %d bytes, want %d", key, len(got), len(payload)) // Delete, then confirm the object is gone from both GET and List. _, err = cli.DeleteObject(&s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: aws.String(key)}) require.NoError(t, err, "delete %s", key) _, err = cli.GetObject(&s3.GetObjectInput{Bucket: aws.String(bucket), Key: aws.String(key)}) require.True(t, isPredastoreNoSuchKey(err), "get %s after delete: want NoSuchKey, got %v", key, err) list, err := cli.ListObjectsV2(&s3.ListObjectsV2Input{Bucket: aws.String(bucket), Prefix: aws.String(key)}) require.NoError(t, err, "list after delete") require.Lenf(t, list.Contents, 0, "list after delete returned %d objects, want 0", len(list.Contents)) // The store must keep serving once the delete has landed: a fresh write // round-trips, proving the delete did not wedge the bucket. const key2 = "lifecycle/after-delete" t.Cleanup(func() { _, _ = cli.DeleteObject(&s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: aws.String(key2)}) }) _, err = cli.PutObject(&s3.PutObjectInput{ Bucket: aws.String(bucket), Key: aws.String(key2), Body: bytes.NewReader(payload), }) require.NoError(t, err, "put %s after delete", key2) _, err = predastoreGetBytes(t, cli, bucket, key2) require.NoError(t, err, "get %s after delete", key2) } // predastoreS3Client builds an aws-sdk-go v1 S3 client against the shared // predastore fixture daemon. TLS verification is skipped: the fixture serves // the S3 API under a self-signed certificate it generates for itself (see // testpredastore.Start), which no trust store knows about. func predastoreS3Client(t *testing.T, fixture *testpredastore.Fixture) *s3.S3 { t.Helper() httpClient := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, } sess, err := session.NewSession(&aws.Config{ Region: aws.String(fixture.Region), Endpoint: aws.String("https://" + fixture.Host), Credentials: credentials.NewStaticCredentials(fixture.AccessKey, fixture.SecretKey, ""), S3ForcePathStyle: aws.Bool(true), HTTPClient: httpClient, }) require.NoError(t, err, "predastore s3 session") return s3.New(sess) } // predastoreGetBytes fetches an object and returns its full body. func predastoreGetBytes(t *testing.T, cli *s3.S3, bucket, key string) ([]byte, error) { t.Helper() out, err := cli.GetObject(&s3.GetObjectInput{Bucket: aws.String(bucket), Key: aws.String(key)}) if err != nil { return nil, err } defer func() { _ = out.Body.Close() }() buf := new(bytes.Buffer) if _, err := buf.ReadFrom(out.Body); err != nil { return nil, err } return buf.Bytes(), nil } // isPredastoreNoSuchKey reports whether err is the S3 NoSuchKey error // predastore returns for a GET on a deleted or missing key. func isPredastoreNoSuchKey(err error) bool { var aerr awserr.Error if errors.As(err, &aerr) { return aerr.Code() == s3.ErrCodeNoSuchKey } return false }