Files
spinifex/tests/integration/predastore_test.go
T
Josh Siviour fd8be8a814 refactor(test): route duplicated NATS bootstraps through testutil (#776)
* refactor(test): route duplicated NATS bootstraps through testutil

Thirteen test sites hand-rolled a NATS or JetStream server bootstrap that
spinifex/testutil already provides, and one seedKV clone duplicated
testutil.SeedKV. Route them through the shared helpers.

Sites whose options genuinely diverge are left alone: fixed ports needed for
reconnect and restart scenarios, custom TLS and token auth, a custom
MaxPayload, and TestMain entry points that have no testing.TB for t.Cleanup.
The natgw and routetable seedKV clones use CreateOrUpdateKeyValue rather than
CreateKeyValue, so their failure behaviour differs and they stay as they are.

Also moves eight test files to external test packages where they declare no
helper used by a sibling in the same package. Coverage is unaffected: Go
compiles internal and external test packages into one binary per package, so
attribution is identical either way.

* chore(test): require new test files to use an external test package

Adds a preflight check that fails when a newly added test file declares an
in-package test. External test packages bind tests to the exported API and
let packages share test doubles, which an in-package double cannot be. Only
added files are checked, so the existing in-package tests are untouched.

e2e and integration suites are exempt: both share a package-level fixture
built in main_test.go, so their files cannot move individually. A file that
needs unexported access can opt out with a //test:in-package comment.
2026-08-12 22:38:15 +10:00

131 lines
5.0 KiB
Go

//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}, //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
}