diff --git a/tests/e2e/single/predastore_test.go b/tests/e2e/single/predastore_test.go deleted file mode 100644 index b82cd8a98..000000000 --- a/tests/e2e/single/predastore_test.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build e2e - -package single - -import ( - "context" - "testing" - - "github.com/mulgadc/spinifex/tests/e2e/harness" -) - -// runPredastoreObjectLifecycle drives predastore's user-visible object contract -// (write → read → delete → gone → still-serving) against the deployed S3 -// endpoint. Predastore shares the host the AWS gateway binds, so resolve it the -// same way harness.NewAWSClient does. -func runPredastoreObjectLifecycle(t *testing.T, fix *Fixture) { - host := "127.0.0.1" - if len(fix.Env.ServiceIPs) > 0 { - host = fix.Env.ServiceIPs[0] - } - harness.AssertPredastoreObjectLifecycle(context.Background(), t, host) -} diff --git a/tests/e2e/single/tests_test.go b/tests/e2e/single/tests_test.go index 566538f67..ea13fce83 100644 --- a/tests/e2e/single/tests_test.go +++ b/tests/e2e/single/tests_test.go @@ -30,13 +30,6 @@ func TestAccountScoping(t *testing.T) { runAccountScoping(t, requireSingleNodeFixture(t)) } -// TestPredastoreObjectLifecycle owns a dedicated bucket and never touches the -// singleton VM or default VPC/SG, so it is parallel-safe. -func TestPredastoreObjectLifecycle(t *testing.T) { - t.Parallel() - runPredastoreObjectLifecycle(t, requireSingleNodeFixture(t)) -} - func TestVPCSubnetE2E(t *testing.T) { t.Parallel() runVPCSubnetE2E(t, requireSingleNodeFixture(t)) diff --git a/tests/integration/predastore_test.go b/tests/integration/predastore_test.go new file mode 100644 index 000000000..74e4a07c7 --- /dev/null +++ b/tests/integration/predastore_test.go @@ -0,0 +1,131 @@ +//go:build integration + +package integration + +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's +// self-signed certificate is trusted process-wide for quicclient/the s3db +// REST client (see testpredastore.Start), but a plain s3.S3 built here has +// its own independent HTTP transport. +func predastoreS3Client(t *testing.T, fixture *testpredastore.Fixture) *s3.S3 { + t.Helper() + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // self-signed cert generated by this same fixture, localhost-only + }, + } + 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 +}