test(integration): migrate predastore object lifecycle off live guests

runPredastoreObjectLifecycle only needs a real predastore daemon, not a
tofu-provisioned cluster: it now runs against tests/fixtures/predastore's
in-process fixture, covering the same create/put/get/delete/list contract.
This commit is contained in:
joshsiv-mulga
2026-07-20 23:47:40 +10:00
parent 7ce592227b
commit 177fddd308
3 changed files with 131 additions and 29 deletions
-22
View File
@@ -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)
}
-7
View File
@@ -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))
+131
View File
@@ -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
}