second first commit

This commit is contained in:
Zakaria
2026-09-22 08:57:00 -04:00
parent e88d7e2ff3
commit 0aef7fb2d8
4766 changed files with 1236986 additions and 0 deletions
@@ -0,0 +1,311 @@
package tests
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"strings"
"testing"
"time"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/acm"
acmtypes "github.com/aws/aws-sdk-go-v2/service/acm/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// generateSelfSignedCert creates a self-signed certificate and private key in PEM format.
func generateSelfSignedCert() (certPEM, keyPEM []byte) {
key, _ := rsa.GenerateKey(rand.Reader, 2048)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "test.example.com"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
}
certDER, _ := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
return
}
func TestACM(t *testing.T) {
ctx := context.Background()
svc := testutil.ACMClient()
var certARN string
t.Run("RequestCertificate", func(t *testing.T) {
r, err := svc.RequestCertificate(ctx, &acm.RequestCertificateInput{
DomainName: aws.String("go-test.example.com"),
})
require.NoError(t, err)
certARN = aws.ToString(r.CertificateArn)
assert.NotEmpty(t, certARN)
assert.True(t, strings.HasPrefix(certARN, "arn:aws:acm:"), "ARN should match arn:aws:acm pattern, got: %s", certARN)
})
t.Run("DescribeCertificate", func(t *testing.T) {
if certARN == "" {
t.Skip("No certificate ARN from RequestCertificate")
}
r, err := svc.DescribeCertificate(ctx, &acm.DescribeCertificateInput{
CertificateArn: aws.String(certARN),
})
require.NoError(t, err)
assert.Equal(t, "go-test.example.com", aws.ToString(r.Certificate.DomainName))
assert.Equal(t, acmtypes.CertificateStatusIssued, r.Certificate.Status)
})
t.Run("GetCertificate", func(t *testing.T) {
if certARN == "" {
t.Skip("No certificate ARN from RequestCertificate")
}
r, err := svc.GetCertificate(ctx, &acm.GetCertificateInput{
CertificateArn: aws.String(certARN),
})
require.NoError(t, err)
assert.NotEmpty(t, aws.ToString(r.Certificate))
})
t.Run("ListCertificates", func(t *testing.T) {
if certARN == "" {
t.Skip("No certificate ARN from RequestCertificate")
}
r, err := svc.ListCertificates(ctx, &acm.ListCertificatesInput{})
require.NoError(t, err)
found := false
for _, c := range r.CertificateSummaryList {
if aws.ToString(c.CertificateArn) == certARN {
found = true
break
}
}
assert.True(t, found, "Certificate %s should appear in list", certARN)
})
t.Run("AddTagsToCertificate", func(t *testing.T) {
if certARN == "" {
t.Skip("No certificate ARN from RequestCertificate")
}
_, err := svc.AddTagsToCertificate(ctx, &acm.AddTagsToCertificateInput{
CertificateArn: aws.String(certARN),
Tags: []acmtypes.Tag{
{Key: aws.String("Env"), Value: aws.String("test")},
},
})
require.NoError(t, err)
r, err := svc.ListTagsForCertificate(ctx, &acm.ListTagsForCertificateInput{
CertificateArn: aws.String(certARN),
})
require.NoError(t, err)
found := false
for _, tag := range r.Tags {
if aws.ToString(tag.Key) == "Env" && aws.ToString(tag.Value) == "test" {
found = true
break
}
}
assert.True(t, found, "Tag Env=test should be present")
})
t.Run("ListTagsForCertificate", func(t *testing.T) {
if certARN == "" {
t.Skip("No certificate ARN from RequestCertificate")
}
r, err := svc.ListTagsForCertificate(ctx, &acm.ListTagsForCertificateInput{
CertificateArn: aws.String(certARN),
})
require.NoError(t, err)
found := false
for _, tag := range r.Tags {
if aws.ToString(tag.Key) == "Env" && aws.ToString(tag.Value) == "test" {
found = true
break
}
}
assert.True(t, found, "Tag Env=test should still be present")
})
t.Run("RemoveTagsFromCertificate", func(t *testing.T) {
if certARN == "" {
t.Skip("No certificate ARN from RequestCertificate")
}
_, err := svc.RemoveTagsFromCertificate(ctx, &acm.RemoveTagsFromCertificateInput{
CertificateArn: aws.String(certARN),
Tags: []acmtypes.Tag{
{Key: aws.String("Env"), Value: aws.String("test")},
},
})
require.NoError(t, err)
r, err := svc.ListTagsForCertificate(ctx, &acm.ListTagsForCertificateInput{
CertificateArn: aws.String(certARN),
})
require.NoError(t, err)
for _, tag := range r.Tags {
assert.NotEqual(t, "Env", aws.ToString(tag.Key), "Tag Env should have been removed")
}
})
t.Run("DeleteCertificate", func(t *testing.T) {
if certARN == "" {
t.Skip("No certificate ARN from RequestCertificate")
}
_, err := svc.DeleteCertificate(ctx, &acm.DeleteCertificateInput{
CertificateArn: aws.String(certARN),
})
require.NoError(t, err)
_, err = svc.DescribeCertificate(ctx, &acm.DescribeCertificateInput{
CertificateArn: aws.String(certARN),
})
assert.Error(t, err, "DescribeCertificate should fail after deletion")
})
}
func TestACMImportExport(t *testing.T) {
ctx := context.Background()
svc := testutil.ACMClient()
certPEM, keyPEM := generateSelfSignedCert()
var importedARN string
t.Cleanup(func() {
if importedARN != "" {
svc.DeleteCertificate(ctx, &acm.DeleteCertificateInput{
CertificateArn: aws.String(importedARN),
})
}
})
t.Run("ImportCertificate", func(t *testing.T) {
r, err := svc.ImportCertificate(ctx, &acm.ImportCertificateInput{
Certificate: certPEM,
PrivateKey: keyPEM,
})
require.NoError(t, err)
importedARN = aws.ToString(r.CertificateArn)
assert.NotEmpty(t, importedARN)
assert.True(t, strings.HasPrefix(importedARN, "arn:aws:acm:"), "ARN should match arn:aws:acm pattern, got: %s", importedARN)
})
t.Run("GetImportedCertificate", func(t *testing.T) {
if importedARN == "" {
t.Skip("No imported certificate ARN")
}
r, err := svc.GetCertificate(ctx, &acm.GetCertificateInput{
CertificateArn: aws.String(importedARN),
})
require.NoError(t, err)
assert.NotEmpty(t, aws.ToString(r.Certificate))
})
t.Run("ExportCertificate", func(t *testing.T) {
if importedARN == "" {
t.Skip("No imported certificate ARN")
}
r, err := svc.ExportCertificate(ctx, &acm.ExportCertificateInput{
CertificateArn: aws.String(importedARN),
Passphrase: []byte("test-passphrase"),
})
require.NoError(t, err)
assert.NotEmpty(t, aws.ToString(r.Certificate), "Exported certificate body should not be empty")
assert.NotEmpty(t, aws.ToString(r.PrivateKey), "Exported private key should not be empty")
})
t.Run("ExportRequestedCertificateFails", func(t *testing.T) {
// Request a certificate (not imported) - export should fail
reqResult, err := svc.RequestCertificate(ctx, &acm.RequestCertificateInput{
DomainName: aws.String("go-export-fail.example.com"),
})
require.NoError(t, err)
requestedARN := aws.ToString(reqResult.CertificateArn)
t.Cleanup(func() {
svc.DeleteCertificate(ctx, &acm.DeleteCertificateInput{
CertificateArn: aws.String(requestedARN),
})
})
_, err = svc.ExportCertificate(ctx, &acm.ExportCertificateInput{
CertificateArn: aws.String(requestedARN),
Passphrase: []byte("test-passphrase"),
})
assert.Error(t, err, "ExportCertificate should fail for a requested (non-imported) certificate")
})
}
func TestACMAccountConfiguration(t *testing.T) {
ctx := context.Background()
svc := testutil.ACMClient()
_, err := svc.PutAccountConfiguration(ctx, &acm.PutAccountConfigurationInput{
ExpiryEvents: &acmtypes.ExpiryEventsConfiguration{
DaysBeforeExpiry: aws.Int32(45),
},
IdempotencyToken: aws.String("go-test-idempotency-token"),
})
require.NoError(t, err)
r, err := svc.GetAccountConfiguration(ctx, &acm.GetAccountConfigurationInput{})
require.NoError(t, err)
require.NotNil(t, r.ExpiryEvents)
assert.Equal(t, int32(45), aws.ToInt32(r.ExpiryEvents.DaysBeforeExpiry))
}
func TestACMErrorHandling(t *testing.T) {
ctx := context.Background()
svc := testutil.ACMClient()
t.Run("DescribeNonExistent", func(t *testing.T) {
_, err := svc.DescribeCertificate(ctx, &acm.DescribeCertificateInput{
CertificateArn: aws.String("arn:aws:acm:us-east-1:000000000000:certificate/00000000-0000-0000-0000-000000000000"),
})
assert.Error(t, err, "DescribeCertificate should fail for non-existent ARN")
})
t.Run("RequestWithSANs", func(t *testing.T) {
r, err := svc.RequestCertificate(ctx, &acm.RequestCertificateInput{
DomainName: aws.String("go-san-test.example.com"),
SubjectAlternativeNames: []string{
"go-san-test.example.com",
"www.go-san-test.example.com",
},
})
require.NoError(t, err)
sanARN := aws.ToString(r.CertificateArn)
assert.NotEmpty(t, sanARN)
t.Cleanup(func() {
svc.DeleteCertificate(ctx, &acm.DeleteCertificateInput{
CertificateArn: aws.String(sanARN),
})
})
desc, err := svc.DescribeCertificate(ctx, &acm.DescribeCertificateInput{
CertificateArn: aws.String(sanARN),
})
require.NoError(t, err)
assert.Contains(t, desc.Certificate.SubjectAlternativeNames, "go-san-test.example.com")
assert.Contains(t, desc.Certificate.SubjectAlternativeNames, "www.go-san-test.example.com")
})
t.Run("ImportInvalidPEM", func(t *testing.T) {
_, err := svc.ImportCertificate(ctx, &acm.ImportCertificateInput{
Certificate: []byte("not-valid-pem-data"),
PrivateKey: []byte("not-valid-key-data"),
})
assert.Error(t, err, "ImportCertificate should fail with invalid PEM data")
})
}
@@ -0,0 +1,164 @@
package tests
import (
"context"
"testing"
"time"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cloudwatch"
cwtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatch/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCloudWatch(t *testing.T) {
ctx := context.Background()
svc := testutil.CloudWatchClient()
namespace := "GoTest"
t.Run("PutMetricData", func(t *testing.T) {
_, err := svc.PutMetricData(ctx, &cloudwatch.PutMetricDataInput{
Namespace: aws.String(namespace),
MetricData: []cwtypes.MetricDatum{
{
MetricName: aws.String("RequestCount"),
Value: aws.Float64(42),
Unit: cwtypes.StandardUnitCount,
Timestamp: aws.Time(time.Now()),
},
},
})
require.NoError(t, err)
})
t.Run("ListMetrics", func(t *testing.T) {
r, err := svc.ListMetrics(ctx, &cloudwatch.ListMetricsInput{Namespace: aws.String(namespace)})
require.NoError(t, err)
assert.NotEmpty(t, r.Metrics)
})
t.Run("GetMetricStatistics", func(t *testing.T) {
now := time.Now()
r, err := svc.GetMetricStatistics(ctx, &cloudwatch.GetMetricStatisticsInput{
Namespace: aws.String(namespace),
MetricName: aws.String("RequestCount"),
StartTime: aws.Time(now.Add(-5 * time.Minute)),
EndTime: aws.Time(now.Add(time.Minute)),
Period: aws.Int32(60),
Statistics: []cwtypes.Statistic{cwtypes.StatisticSum},
})
require.NoError(t, err)
assert.NotEmpty(t, r.Datapoints)
})
t.Run("PutMetricDataWithStatisticValues", func(t *testing.T) {
now := time.Now()
namespace := "GoTestStatisticValues"
// Put metric data with pre-calculated statistics
_, err := svc.PutMetricData(ctx, &cloudwatch.PutMetricDataInput{
Namespace: aws.String(namespace),
MetricData: []cwtypes.MetricDatum{
{
MetricName: aws.String("AggregatedMetric"),
StatisticValues: &cwtypes.StatisticSet{
SampleCount: aws.Float64(5.0),
Sum: aws.Float64(150.0),
Minimum: aws.Float64(20.0),
Maximum: aws.Float64(40.0),
},
Unit: cwtypes.StandardUnitCount,
},
},
})
require.NoError(t, err)
// Query back the statistics
r, err := svc.GetMetricStatistics(ctx, &cloudwatch.GetMetricStatisticsInput{
Namespace: aws.String(namespace),
MetricName: aws.String("AggregatedMetric"),
StartTime: aws.Time(now.Add(-time.Hour)),
EndTime: aws.Time(now.Add(time.Minute)),
Period: aws.Int32(3600),
Statistics: []cwtypes.Statistic{
cwtypes.StatisticSum,
cwtypes.StatisticAverage,
cwtypes.StatisticMinimum,
cwtypes.StatisticMaximum,
cwtypes.StatisticSampleCount,
},
})
require.NoError(t, err)
assert.NotEmpty(t, r.Datapoints)
dp := r.Datapoints[0]
assert.NotNil(t, dp.Sum)
assert.Equal(t, 150.0, *dp.Sum)
assert.NotNil(t, dp.SampleCount)
assert.Equal(t, float64(5), *dp.SampleCount)
assert.NotNil(t, dp.Minimum)
assert.Equal(t, 20.0, *dp.Minimum)
assert.NotNil(t, dp.Maximum)
assert.Equal(t, 40.0, *dp.Maximum)
assert.NotNil(t, dp.Average)
assert.Equal(t, 30.0, *dp.Average) // sum / sampleCount
})
t.Run("GetMetricData", func(t *testing.T) {
now := time.Now()
namespace := "GoTestGetMetricData"
_, err := svc.PutMetricData(ctx, &cloudwatch.PutMetricDataInput{
Namespace: aws.String(namespace),
MetricData: []cwtypes.MetricDatum{
{
MetricName: aws.String("RequestCount"),
Value: aws.Float64(123),
Unit: cwtypes.StandardUnitCount,
Timestamp: aws.Time(now),
},
},
})
require.NoError(t, err)
// GetMetricData round-trips through the rpc-v2-cbor protocol. The response
// MetricDataResults[].Timestamps list must be encoded as CBOR tag(1)
// timestamps; otherwise the SDK fails to decode it (unexpected value type
// cbor.Uint), so this require.NoError is the core regression guard.
r, err := svc.GetMetricData(ctx, &cloudwatch.GetMetricDataInput{
StartTime: aws.Time(now.Add(-5 * time.Minute)),
EndTime: aws.Time(now.Add(5 * time.Minute)),
MetricDataQueries: []cwtypes.MetricDataQuery{
{
Id: aws.String("m1"),
MetricStat: &cwtypes.MetricStat{
Metric: &cwtypes.Metric{
Namespace: aws.String(namespace),
MetricName: aws.String("RequestCount"),
},
Period: aws.Int32(60),
Stat: aws.String("Sum"),
},
ReturnData: aws.Bool(true),
},
},
})
require.NoError(t, err)
require.NotEmpty(t, r.MetricDataResults)
var result *cwtypes.MetricDataResult
for i := range r.MetricDataResults {
if r.MetricDataResults[i].Id != nil && *r.MetricDataResults[i].Id == "m1" {
result = &r.MetricDataResults[i]
break
}
}
require.NotNil(t, result, "expected a MetricDataResult with Id \"m1\" in the response")
assert.NotEmpty(t, result.Timestamps)
assert.NotEmpty(t, result.Values)
})
}
@@ -0,0 +1,62 @@
package tests
import (
"context"
"testing"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCognitoDescribeUserPoolStandardAttributes(t *testing.T) {
ctx := context.Background()
svc := testutil.CognitoClient()
poolName := "go-test-cognito-standard-attrs"
created, err := svc.CreateUserPool(ctx, &cognitoidentityprovider.CreateUserPoolInput{
PoolName: aws.String(poolName),
})
require.NoError(t, err)
poolID := created.UserPool.Id
t.Cleanup(func() {
svc.DeleteUserPool(ctx, &cognitoidentityprovider.DeleteUserPoolInput{
UserPoolId: poolID,
})
})
resp, err := svc.DescribeUserPool(ctx, &cognitoidentityprovider.DescribeUserPoolInput{
UserPoolId: poolID,
})
require.NoError(t, err)
schema := resp.UserPool.SchemaAttributes
assert.Len(t, schema, 20, "DescribeUserPool must return all 20 standard Cognito attributes")
names := make(map[string]bool, len(schema))
for _, attr := range schema {
names[aws.ToString(attr.Name)] = true
}
expected := []string{
"sub", "name", "given_name", "family_name", "middle_name", "nickname",
"preferred_username", "profile", "picture", "website", "email",
"email_verified", "gender", "birthdate", "zoneinfo", "locale",
"phone_number", "phone_number_verified", "address", "updated_at",
}
for _, attr := range expected {
assert.True(t, names[attr], "missing standard attribute: %s", attr)
}
// spot-check sub
for _, attr := range schema {
if aws.ToString(attr.Name) == "sub" {
assert.True(t, aws.ToBool(attr.Required), "sub must be Required")
assert.False(t, aws.ToBool(attr.Mutable), "sub must not be Mutable")
}
}
}
@@ -0,0 +1,129 @@
package tests
import (
"context"
"testing"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
ddbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDynamoDB(t *testing.T) {
ctx := context.Background()
svc := testutil.DynamoDBClient()
table := "go-test-table"
t.Cleanup(func() {
svc.DeleteTable(ctx, &dynamodb.DeleteTableInput{TableName: aws.String(table)})
})
t.Run("CreateTable", func(t *testing.T) {
_, err := svc.CreateTable(ctx, &dynamodb.CreateTableInput{
TableName: aws.String(table),
BillingMode: ddbtypes.BillingModePayPerRequest,
AttributeDefinitions: []ddbtypes.AttributeDefinition{
{AttributeName: aws.String("pk"), AttributeType: ddbtypes.ScalarAttributeTypeS},
{AttributeName: aws.String("sk"), AttributeType: ddbtypes.ScalarAttributeTypeS},
},
KeySchema: []ddbtypes.KeySchemaElement{
{AttributeName: aws.String("pk"), KeyType: ddbtypes.KeyTypeHash},
{AttributeName: aws.String("sk"), KeyType: ddbtypes.KeyTypeRange},
},
})
require.NoError(t, err)
})
t.Run("DescribeTable", func(t *testing.T) {
r, err := svc.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: aws.String(table)})
require.NoError(t, err)
assert.Equal(t, ddbtypes.TableStatusActive, r.Table.TableStatus)
})
t.Run("ListTables", func(t *testing.T) {
r, err := svc.ListTables(ctx, &dynamodb.ListTablesInput{})
require.NoError(t, err)
assert.NotEmpty(t, r.TableNames)
})
t.Run("PutItem", func(t *testing.T) {
_, err := svc.PutItem(ctx, &dynamodb.PutItemInput{
TableName: aws.String(table),
Item: map[string]ddbtypes.AttributeValue{
"pk": &ddbtypes.AttributeValueMemberS{Value: "user#1"},
"sk": &ddbtypes.AttributeValueMemberS{Value: "profile"},
"name": &ddbtypes.AttributeValueMemberS{Value: "Alice"},
"age": &ddbtypes.AttributeValueMemberN{Value: "30"},
},
})
require.NoError(t, err)
})
t.Run("GetItem", func(t *testing.T) {
r, err := svc.GetItem(ctx, &dynamodb.GetItemInput{
TableName: aws.String(table),
Key: map[string]ddbtypes.AttributeValue{
"pk": &ddbtypes.AttributeValueMemberS{Value: "user#1"},
"sk": &ddbtypes.AttributeValueMemberS{Value: "profile"},
},
})
require.NoError(t, err)
attr, ok := r.Item["name"].(*ddbtypes.AttributeValueMemberS)
require.True(t, ok)
assert.Equal(t, "Alice", attr.Value)
})
t.Run("UpdateItem", func(t *testing.T) {
_, err := svc.UpdateItem(ctx, &dynamodb.UpdateItemInput{
TableName: aws.String(table),
Key: map[string]ddbtypes.AttributeValue{
"pk": &ddbtypes.AttributeValueMemberS{Value: "user#1"},
"sk": &ddbtypes.AttributeValueMemberS{Value: "profile"},
},
UpdateExpression: aws.String("SET #n = :n"),
ExpressionAttributeNames: map[string]string{"#n": "name"},
ExpressionAttributeValues: map[string]ddbtypes.AttributeValue{
":n": &ddbtypes.AttributeValueMemberS{Value: "Alice Updated"},
},
})
require.NoError(t, err)
})
t.Run("Query", func(t *testing.T) {
r, err := svc.Query(ctx, &dynamodb.QueryInput{
TableName: aws.String(table),
KeyConditionExpression: aws.String("pk = :pk"),
ExpressionAttributeValues: map[string]ddbtypes.AttributeValue{
":pk": &ddbtypes.AttributeValueMemberS{Value: "user#1"},
},
})
require.NoError(t, err)
assert.NotEmpty(t, r.Items)
})
t.Run("Scan", func(t *testing.T) {
r, err := svc.Scan(ctx, &dynamodb.ScanInput{TableName: aws.String(table)})
require.NoError(t, err)
assert.NotEmpty(t, r.Items)
})
t.Run("DeleteItem", func(t *testing.T) {
_, err := svc.DeleteItem(ctx, &dynamodb.DeleteItemInput{
TableName: aws.String(table),
Key: map[string]ddbtypes.AttributeValue{
"pk": &ddbtypes.AttributeValueMemberS{Value: "user#1"},
"sk": &ddbtypes.AttributeValueMemberS{Value: "profile"},
},
})
require.NoError(t, err)
})
t.Run("DeleteTable", func(t *testing.T) {
_, err := svc.DeleteTable(ctx, &dynamodb.DeleteTableInput{TableName: aws.String(table)})
require.NoError(t, err)
})
}
@@ -0,0 +1,138 @@
package tests
import (
"context"
"encoding/base64"
"errors"
"strings"
"testing"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecr"
ecrtypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestECR is the Go control-plane compatibility suite for Floci's emulated ECR.
// Test-first: this file is committed before the server implementation lands.
func TestECR(t *testing.T) {
ctx := context.Background()
svc := testutil.ECRClient()
const repoName = "floci-it/app-go"
// Cleanup helper
cleanup := func(name string) {
_, _ = svc.DeleteRepository(ctx, &ecr.DeleteRepositoryInput{
RepositoryName: aws.String(name),
Force: true,
})
}
t.Cleanup(func() { cleanup(repoName) })
t.Run("CreateRepository", func(t *testing.T) {
out, err := svc.CreateRepository(ctx, &ecr.CreateRepositoryInput{
RepositoryName: aws.String(repoName),
})
require.NoError(t, err)
require.NotNil(t, out.Repository)
assert.Equal(t, repoName, aws.ToString(out.Repository.RepositoryName))
assert.Contains(t, aws.ToString(out.Repository.RepositoryArn), ":repository/"+repoName)
uri := aws.ToString(out.Repository.RepositoryUri)
assert.Contains(t, uri, "/"+repoName)
assert.True(t, strings.Contains(uri, "localhost:"), "repositoryUri should target loopback: %s", uri)
})
t.Run("CreateRepositoryDuplicate", func(t *testing.T) {
_, err := svc.CreateRepository(ctx, &ecr.CreateRepositoryInput{
RepositoryName: aws.String(repoName),
})
require.Error(t, err)
var alreadyExists *ecrtypes.RepositoryAlreadyExistsException
assert.True(t, errors.As(err, &alreadyExists), "expected RepositoryAlreadyExistsException, got %T", err)
})
t.Run("DescribeRepositories", func(t *testing.T) {
out, err := svc.DescribeRepositories(ctx, &ecr.DescribeRepositoriesInput{
RepositoryNames: []string{repoName},
})
require.NoError(t, err)
require.Len(t, out.Repositories, 1)
assert.Equal(t, repoName, aws.ToString(out.Repositories[0].RepositoryName))
})
t.Run("GetAuthorizationToken", func(t *testing.T) {
out, err := svc.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})
require.NoError(t, err)
require.NotEmpty(t, out.AuthorizationData)
data := out.AuthorizationData[0]
token := aws.ToString(data.AuthorizationToken)
require.NotEmpty(t, token)
assert.True(t, strings.HasPrefix(aws.ToString(data.ProxyEndpoint), "http"))
decoded, err := base64.StdEncoding.DecodeString(token)
require.NoError(t, err)
assert.True(t, strings.HasPrefix(string(decoded), "AWS:"), "decoded token must start with AWS:")
})
t.Run("ListImagesEmpty", func(t *testing.T) {
out, err := svc.ListImages(ctx, &ecr.ListImagesInput{
RepositoryName: aws.String(repoName),
})
require.NoError(t, err)
assert.Empty(t, out.ImageIds)
})
t.Run("PutImageTagMutability", func(t *testing.T) {
out, err := svc.PutImageTagMutability(ctx, &ecr.PutImageTagMutabilityInput{
RepositoryName: aws.String(repoName),
ImageTagMutability: ecrtypes.ImageTagMutabilityImmutable,
})
require.NoError(t, err)
assert.Equal(t, ecrtypes.ImageTagMutabilityImmutable, out.ImageTagMutability)
})
t.Run("LifecyclePolicyRoundTrip", func(t *testing.T) {
policy := `{"rules":[{"rulePriority":1,"selection":{"tagStatus":"untagged","countType":"imageCountMoreThan","countNumber":5},"action":{"type":"expire"}}]}`
_, err := svc.PutLifecyclePolicy(ctx, &ecr.PutLifecyclePolicyInput{
RepositoryName: aws.String(repoName),
LifecyclePolicyText: aws.String(policy),
})
require.NoError(t, err)
got, err := svc.GetLifecyclePolicy(ctx, &ecr.GetLifecyclePolicyInput{
RepositoryName: aws.String(repoName),
})
require.NoError(t, err)
assert.Equal(t, policy, aws.ToString(got.LifecyclePolicyText))
})
t.Run("RepositoryPolicyRoundTrip", func(t *testing.T) {
policy := `{"Version":"2012-10-17","Statement":[{"Sid":"AllowAll","Effect":"Allow","Principal":"*","Action":"ecr:*"}]}`
_, err := svc.SetRepositoryPolicy(ctx, &ecr.SetRepositoryPolicyInput{
RepositoryName: aws.String(repoName),
PolicyText: aws.String(policy),
})
require.NoError(t, err)
got, err := svc.GetRepositoryPolicy(ctx, &ecr.GetRepositoryPolicyInput{
RepositoryName: aws.String(repoName),
})
require.NoError(t, err)
assert.Equal(t, policy, aws.ToString(got.PolicyText))
})
t.Run("DeleteRepositoryForce", func(t *testing.T) {
_, err := svc.DeleteRepository(ctx, &ecr.DeleteRepositoryInput{
RepositoryName: aws.String(repoName),
Force: true,
})
require.NoError(t, err)
_, err = svc.DescribeRepositories(ctx, &ecr.DescribeRepositoriesInput{
RepositoryNames: []string{repoName},
})
require.Error(t, err)
var notFound *ecrtypes.RepositoryNotFoundException
assert.True(t, errors.As(err, &notFound), "expected RepositoryNotFoundException, got %T", err)
})
}
@@ -0,0 +1,57 @@
package tests
import (
"context"
"fmt"
"testing"
"time"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/glue"
gluetypes "github.com/aws/aws-sdk-go-v2/service/glue/types"
"github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGlueCreateDatabaseTagsAreVisibleViaResourceGroupsTaggingAPI(t *testing.T) {
ctx := context.Background()
glueClient := testutil.GlueClient()
taggingClient := testutil.ResourceGroupsTaggingAPIClient()
databaseName := fmt.Sprintf("go-glue-db-%d", time.Now().UnixNano())
databaseArn := fmt.Sprintf("arn:aws:glue:us-east-1:000000000000:database/%s", databaseName)
t.Cleanup(func() {
_, _ = glueClient.DeleteDatabase(ctx, &glue.DeleteDatabaseInput{Name: aws.String(databaseName)})
})
_, err := glueClient.CreateDatabase(ctx, &glue.CreateDatabaseInput{
DatabaseInput: &gluetypes.DatabaseInput{
Name: aws.String(databaseName),
Description: aws.String("go glue database tags"),
},
Tags: map[string]string{
"Environment": "dev",
"Project": "project1",
},
})
require.NoError(t, err)
response, err := taggingClient.GetResources(ctx, &resourcegroupstaggingapi.GetResourcesInput{
ResourceARNList: []string{databaseArn},
})
require.NoError(t, err)
require.Len(t, response.ResourceTagMappingList, 1)
mapping := response.ResourceTagMappingList[0]
assert.Equal(t, databaseArn, aws.ToString(mapping.ResourceARN))
tags := map[string]string{}
for _, tag := range mapping.Tags {
tags[aws.ToString(tag.Key)] = aws.ToString(tag.Value)
}
assert.Equal(t, "dev", tags["Environment"])
assert.Equal(t, "project1", tags["Project"])
}
@@ -0,0 +1,49 @@
package tests
import (
"context"
"testing"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIAM(t *testing.T) {
ctx := context.Background()
svc := testutil.IAMClient()
roleName := "go-test-role"
assumePolicy := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}`
t.Cleanup(func() {
svc.DeleteRole(ctx, &iam.DeleteRoleInput{RoleName: aws.String(roleName)})
})
t.Run("CreateRole", func(t *testing.T) {
_, err := svc.CreateRole(ctx, &iam.CreateRoleInput{
RoleName: aws.String(roleName),
AssumeRolePolicyDocument: aws.String(assumePolicy),
})
require.NoError(t, err)
})
t.Run("GetRole", func(t *testing.T) {
r, err := svc.GetRole(ctx, &iam.GetRoleInput{RoleName: aws.String(roleName)})
require.NoError(t, err)
assert.Equal(t, roleName, aws.ToString(r.Role.RoleName))
})
t.Run("ListRoles", func(t *testing.T) {
r, err := svc.ListRoles(ctx, &iam.ListRolesInput{})
require.NoError(t, err)
assert.NotEmpty(t, r.Roles)
})
t.Run("DeleteRole", func(t *testing.T) {
_, err := svc.DeleteRole(ctx, &iam.DeleteRoleInput{RoleName: aws.String(roleName)})
require.NoError(t, err)
})
}
@@ -0,0 +1,830 @@
package tests
import (
"context"
"encoding/json"
"io"
"net"
"strings"
"testing"
"time"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
ddbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/aws/aws-sdk-go-v2/service/iot"
iottypes "github.com/aws/aws-sdk-go-v2/service/iot/types"
"github.com/aws/aws-sdk-go-v2/service/iotdataplane"
jobsdata "github.com/aws/aws-sdk-go-v2/service/iotjobsdataplane"
jobsdatatypes "github.com/aws/aws-sdk-go-v2/service/iotjobsdataplane/types"
"github.com/aws/aws-sdk-go-v2/service/kinesis"
kinesistypes "github.com/aws/aws-sdk-go-v2/service/kinesis/types"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIoT(t *testing.T) {
ctx := context.Background()
svc := testutil.IoTClient()
t.Run("DescribeEndpoint", func(t *testing.T) {
response, err := svc.DescribeEndpoint(ctx, &iot.DescribeEndpointInput{
EndpointType: aws.String("iot:Data-ATS"),
})
require.NoError(t, err)
assert.NotEmpty(t, aws.ToString(response.EndpointAddress))
})
t.Run("DomainConfigurationLifecycle", func(t *testing.T) {
name := "go-iot-domain"
certificateArn := "arn:aws:acm:us-east-1:000000000000:certificate/11111111-1111-1111-1111-111111111111"
// A leftover from an earlier run must be disabled before it can be deleted.
_, _ = svc.UpdateDomainConfiguration(ctx, &iot.UpdateDomainConfigurationInput{
DomainConfigurationName: aws.String(name),
DomainConfigurationStatus: iottypes.DomainConfigurationStatusDisabled,
})
_, _ = svc.DeleteDomainConfiguration(ctx, &iot.DeleteDomainConfigurationInput{DomainConfigurationName: aws.String(name)})
_, err := svc.DescribeDomainConfiguration(ctx, &iot.DescribeDomainConfigurationInput{DomainConfigurationName: aws.String(name)})
var notFound *iottypes.ResourceNotFoundException
require.ErrorAs(t, err, &notFound)
managed, err := svc.DescribeDomainConfiguration(ctx, &iot.DescribeDomainConfigurationInput{DomainConfigurationName: aws.String("iot:Data-ATS")})
require.NoError(t, err)
assert.Equal(t, iottypes.DomainTypeAwsManaged, managed.DomainType)
assert.Equal(t, iottypes.DomainConfigurationStatusEnabled, managed.DomainConfigurationStatus)
assert.NotEmpty(t, aws.ToString(managed.DomainName))
created, err := svc.CreateDomainConfiguration(ctx, &iot.CreateDomainConfigurationInput{
DomainConfigurationName: aws.String(name),
DomainName: aws.String("iot.go.example.com"),
ServerCertificateArns: []string{certificateArn},
ServiceType: iottypes.ServiceTypeData,
Tags: []iottypes.Tag{{Key: aws.String("env"), Value: aws.String("go")}},
})
require.NoError(t, err)
assert.Equal(t, name, aws.ToString(created.DomainConfigurationName))
assert.True(t, strings.HasPrefix(aws.ToString(created.DomainConfigurationArn),
"arn:aws:iot:us-east-1:000000000000:domainconfiguration/"+name+"/"))
_, err = svc.CreateDomainConfiguration(ctx, &iot.CreateDomainConfigurationInput{
DomainConfigurationName: aws.String(name),
DomainName: aws.String("iot.go.example.com"),
ServerCertificateArns: []string{certificateArn},
})
var alreadyExists *iottypes.ResourceAlreadyExistsException
require.ErrorAs(t, err, &alreadyExists)
described, err := svc.DescribeDomainConfiguration(ctx, &iot.DescribeDomainConfigurationInput{DomainConfigurationName: aws.String(name)})
require.NoError(t, err)
assert.Equal(t, aws.ToString(created.DomainConfigurationArn), aws.ToString(described.DomainConfigurationArn))
assert.Equal(t, "iot.go.example.com", aws.ToString(described.DomainName))
assert.Equal(t, iottypes.DomainConfigurationStatusEnabled, described.DomainConfigurationStatus)
assert.Equal(t, iottypes.ServiceTypeData, described.ServiceType)
assert.Equal(t, iottypes.DomainTypeCustomerManaged, described.DomainType)
require.Len(t, described.ServerCertificates, 1)
assert.Equal(t, certificateArn, aws.ToString(described.ServerCertificates[0].ServerCertificateArn))
assert.Equal(t, iottypes.ServerCertificateStatusValid, described.ServerCertificates[0].ServerCertificateStatus)
assert.NotNil(t, described.LastStatusChangeDate)
listed, err := svc.ListDomainConfigurations(ctx, &iot.ListDomainConfigurationsInput{ServiceType: iottypes.ServiceTypeData})
require.NoError(t, err)
var names []string
for _, summary := range listed.DomainConfigurations {
names = append(names, aws.ToString(summary.DomainConfigurationName))
}
assert.Contains(t, names, name)
// Enabling an already ENABLED configuration is accepted and changes nothing.
enabled, err := svc.UpdateDomainConfiguration(ctx, &iot.UpdateDomainConfigurationInput{
DomainConfigurationName: aws.String(name),
DomainConfigurationStatus: iottypes.DomainConfigurationStatusEnabled,
})
require.NoError(t, err)
assert.Equal(t, aws.ToString(created.DomainConfigurationArn), aws.ToString(enabled.DomainConfigurationArn))
described, err = svc.DescribeDomainConfiguration(ctx, &iot.DescribeDomainConfigurationInput{DomainConfigurationName: aws.String(name)})
require.NoError(t, err)
assert.Equal(t, iottypes.DomainConfigurationStatusEnabled, described.DomainConfigurationStatus)
tags, err := svc.ListTagsForResource(ctx, &iot.ListTagsForResourceInput{ResourceArn: created.DomainConfigurationArn})
require.NoError(t, err)
require.Len(t, tags.Tags, 1)
assert.Equal(t, "env", aws.ToString(tags.Tags[0].Key))
_, err = svc.DeleteDomainConfiguration(ctx, &iot.DeleteDomainConfigurationInput{DomainConfigurationName: aws.String(name)})
var invalid *iottypes.InvalidRequestException
require.ErrorAs(t, err, &invalid)
updated, err := svc.UpdateDomainConfiguration(ctx, &iot.UpdateDomainConfigurationInput{
DomainConfigurationName: aws.String(name),
DomainConfigurationStatus: iottypes.DomainConfigurationStatusDisabled,
})
require.NoError(t, err)
assert.Equal(t, aws.ToString(created.DomainConfigurationArn), aws.ToString(updated.DomainConfigurationArn))
described, err = svc.DescribeDomainConfiguration(ctx, &iot.DescribeDomainConfigurationInput{DomainConfigurationName: aws.String(name)})
require.NoError(t, err)
assert.Equal(t, iottypes.DomainConfigurationStatusDisabled, described.DomainConfigurationStatus)
_, err = svc.DeleteDomainConfiguration(ctx, &iot.DeleteDomainConfigurationInput{DomainConfigurationName: aws.String(name)})
require.NoError(t, err)
_, err = svc.DescribeDomainConfiguration(ctx, &iot.DescribeDomainConfigurationInput{DomainConfigurationName: aws.String(name)})
require.ErrorAs(t, err, &notFound)
})
t.Run("ThingRegistryCrud", func(t *testing.T) {
thingName := "go-iot-thing"
otherThingName := "go-iot-other-thing"
_, _ = svc.DeleteThing(ctx, &iot.DeleteThingInput{ThingName: aws.String(thingName)})
_, _ = svc.DeleteThing(ctx, &iot.DeleteThingInput{ThingName: aws.String(otherThingName)})
_, err := svc.DescribeThing(ctx, &iot.DescribeThingInput{ThingName: aws.String(thingName)})
require.Error(t, err)
created, err := svc.CreateThing(ctx, &iot.CreateThingInput{
ThingName: aws.String(thingName),
AttributePayload: &iottypes.AttributePayload{
Attributes: map[string]string{"env": "go"},
},
})
require.NoError(t, err)
assert.Equal(t, thingName, aws.ToString(created.ThingName))
assert.True(t, strings.HasSuffix(aws.ToString(created.ThingArn), ":thing/"+thingName))
idempotent, err := svc.CreateThing(ctx, &iot.CreateThingInput{
ThingName: aws.String(thingName),
AttributePayload: &iottypes.AttributePayload{
Attributes: map[string]string{"env": "go"},
},
})
require.NoError(t, err)
assert.Equal(t, thingName, aws.ToString(idempotent.ThingName))
_, err = svc.CreateThing(ctx, &iot.CreateThingInput{ThingName: aws.String(thingName)})
require.Error(t, err)
described, err := svc.DescribeThing(ctx, &iot.DescribeThingInput{ThingName: aws.String(thingName)})
require.NoError(t, err)
assert.Equal(t, "go", described.Attributes["env"])
_, err = svc.CreateThing(ctx, &iot.CreateThingInput{ThingName: aws.String(otherThingName)})
require.NoError(t, err)
listed, err := svc.ListThings(ctx, &iot.ListThingsInput{})
require.NoError(t, err)
found := false
for _, thing := range listed.Things {
if aws.ToString(thing.ThingName) == thingName {
found = true
}
}
assert.True(t, found)
firstPage, err := svc.ListThings(ctx, &iot.ListThingsInput{MaxResults: aws.Int32(1)})
require.NoError(t, err)
require.Len(t, firstPage.Things, 1)
require.NotEmpty(t, aws.ToString(firstPage.NextToken))
secondPage, err := svc.ListThings(ctx, &iot.ListThingsInput{MaxResults: aws.Int32(1), NextToken: firstPage.NextToken})
require.NoError(t, err)
require.Len(t, secondPage.Things, 1)
_, err = svc.UpdateThing(ctx, &iot.UpdateThingInput{
ThingName: aws.String(thingName),
AttributePayload: &iottypes.AttributePayload{
Attributes: map[string]string{"env": "updated", "owner": "iot"},
},
})
require.NoError(t, err)
_, err = svc.UpdateThing(ctx, &iot.UpdateThingInput{
ThingName: aws.String(thingName),
ExpectedVersion: aws.Int64(2),
AttributePayload: &iottypes.AttributePayload{Attributes: map[string]string{"env": "versioned", "owner": "iot"}},
})
require.NoError(t, err)
_, err = svc.UpdateThing(ctx, &iot.UpdateThingInput{
ThingName: aws.String(thingName),
ExpectedVersion: aws.Int64(2),
AttributePayload: &iottypes.AttributePayload{Attributes: map[string]string{"env": "stale"}},
})
require.Error(t, err)
assert.Contains(t, err.Error(), "VersionConflictException")
updated, err := svc.DescribeThing(ctx, &iot.DescribeThingInput{ThingName: aws.String(thingName)})
require.NoError(t, err)
assert.Equal(t, "versioned", updated.Attributes["env"])
assert.Equal(t, "iot", updated.Attributes["owner"])
_, err = svc.DeleteThing(ctx, &iot.DeleteThingInput{ThingName: aws.String(thingName)})
require.NoError(t, err)
_, err = svc.DeleteThing(ctx, &iot.DeleteThingInput{ThingName: aws.String(otherThingName)})
require.NoError(t, err)
_, err = svc.DescribeThing(ctx, &iot.DescribeThingInput{ThingName: aws.String(thingName)})
require.Error(t, err)
})
t.Run("ThingTags", func(t *testing.T) {
thingName := "go-iot-tagged-thing"
_, _ = svc.DeleteThing(ctx, &iot.DeleteThingInput{ThingName: aws.String(thingName)})
created, err := svc.CreateThing(ctx, &iot.CreateThingInput{ThingName: aws.String(thingName)})
require.NoError(t, err)
thingArn := aws.ToString(created.ThingArn)
listed, err := svc.ListTagsForResource(ctx, &iot.ListTagsForResourceInput{ResourceArn: aws.String(thingArn)})
require.NoError(t, err)
assert.Empty(t, listed.Tags)
_, err = svc.TagResource(ctx, &iot.TagResourceInput{
ResourceArn: aws.String(thingArn),
Tags: []iottypes.Tag{
{Key: aws.String("env"), Value: aws.String("go")},
{Key: aws.String("owner"), Value: aws.String("iot")},
},
})
require.NoError(t, err)
listed, err = svc.ListTagsForResource(ctx, &iot.ListTagsForResourceInput{ResourceArn: aws.String(thingArn)})
require.NoError(t, err)
assert.Equal(t, map[string]string{"env": "go", "owner": "iot"}, tagsByKey(listed.Tags))
_, err = svc.UntagResource(ctx, &iot.UntagResourceInput{
ResourceArn: aws.String(thingArn),
TagKeys: []string{"env"},
})
require.NoError(t, err)
listed, err = svc.ListTagsForResource(ctx, &iot.ListTagsForResourceInput{ResourceArn: aws.String(thingArn)})
require.NoError(t, err)
assert.Equal(t, map[string]string{"owner": "iot"}, tagsByKey(listed.Tags))
_, err = svc.ListTagsForResource(ctx, &iot.ListTagsForResourceInput{
ResourceArn: aws.String("arn:aws:iot:us-east-1:000000000000:thing/missing-tagged-thing"),
})
require.Error(t, err)
})
t.Run("CertificatesPoliciesAndAttachments", func(t *testing.T) {
createdCert, err := svc.CreateKeysAndCertificate(ctx, &iot.CreateKeysAndCertificateInput{SetAsActive: true})
require.NoError(t, err)
certArn := aws.ToString(createdCert.CertificateArn)
certID := aws.ToString(createdCert.CertificateId)
assert.Contains(t, aws.ToString(createdCert.CertificatePem), "BEGIN CERTIFICATE")
require.NotNil(t, createdCert.KeyPair)
described, err := svc.DescribeCertificate(ctx, &iot.DescribeCertificateInput{CertificateId: aws.String(certID)})
require.NoError(t, err)
assert.Equal(t, iottypes.CertificateStatusActive, described.CertificateDescription.Status)
listedCerts, err := svc.ListCertificates(ctx, &iot.ListCertificatesInput{})
require.NoError(t, err)
assert.NotEmpty(t, listedCerts.Certificates)
_, err = svc.UpdateCertificate(ctx, &iot.UpdateCertificateInput{CertificateId: aws.String(certID), NewStatus: iottypes.CertificateStatusInactive})
require.NoError(t, err)
described, err = svc.DescribeCertificate(ctx, &iot.DescribeCertificateInput{CertificateId: aws.String(certID)})
require.NoError(t, err)
assert.Equal(t, iottypes.CertificateStatusInactive, described.CertificateDescription.Status)
policyName := "go-iot-policy"
policyDocument := `{"Version":"2012-10-17","Statement":[]}`
createdPolicy, err := svc.CreatePolicy(ctx, &iot.CreatePolicyInput{PolicyName: aws.String(policyName), PolicyDocument: aws.String(policyDocument)})
require.NoError(t, err)
assert.Equal(t, policyName, aws.ToString(createdPolicy.PolicyName))
gotPolicy, err := svc.GetPolicy(ctx, &iot.GetPolicyInput{PolicyName: aws.String(policyName)})
require.NoError(t, err)
assert.Contains(t, aws.ToString(gotPolicy.PolicyDocument), "2012-10-17")
listedPolicies, err := svc.ListPolicies(ctx, &iot.ListPoliciesInput{})
require.NoError(t, err)
assert.NotEmpty(t, listedPolicies.Policies)
_, err = svc.AttachPolicy(ctx, &iot.AttachPolicyInput{PolicyName: aws.String(policyName), Target: aws.String(certArn)})
require.NoError(t, err)
_, err = svc.DetachPolicy(ctx, &iot.DetachPolicyInput{PolicyName: aws.String(policyName), Target: aws.String(certArn)})
require.NoError(t, err)
thingName := "go-iot-principal-thing"
_, _ = svc.DeleteThing(ctx, &iot.DeleteThingInput{ThingName: aws.String(thingName)})
_, err = svc.CreateThing(ctx, &iot.CreateThingInput{ThingName: aws.String(thingName)})
require.NoError(t, err)
_, err = svc.AttachThingPrincipal(ctx, &iot.AttachThingPrincipalInput{ThingName: aws.String(thingName), Principal: aws.String(certArn)})
require.NoError(t, err)
principals, err := svc.ListThingPrincipals(ctx, &iot.ListThingPrincipalsInput{ThingName: aws.String(thingName)})
require.NoError(t, err)
assert.Contains(t, principals.Principals, certArn)
_, err = svc.DetachThingPrincipal(ctx, &iot.DetachThingPrincipalInput{ThingName: aws.String(thingName), Principal: aws.String(certArn)})
require.NoError(t, err)
})
t.Run("IoTDataShadowsAndPublish", func(t *testing.T) {
dataSvc := testutil.IoTDataClient()
thingName := "go-iot-shadow-thing"
_, err := dataSvc.DeleteConnection(ctx, &iotdataplane.DeleteConnectionInput{ClientId: aws.String("go-iot-missing-client")})
require.Error(t, err)
assert.Contains(t, err.Error(), "ResourceNotFoundException")
_, err = dataSvc.GetThingShadow(ctx, &iotdataplane.GetThingShadowInput{ThingName: aws.String(thingName)})
require.Error(t, err)
updated, err := dataSvc.UpdateThingShadow(ctx, &iotdataplane.UpdateThingShadowInput{
ThingName: aws.String(thingName),
Payload: []byte(`{"state":{"desired":{"color":"blue"}}}`),
})
require.NoError(t, err)
var shadow map[string]any
require.NoError(t, json.Unmarshal(updated.Payload, &shadow))
assert.Equal(t, float64(1), shadow["version"])
_, err = dataSvc.UpdateThingShadow(ctx, &iotdataplane.UpdateThingShadowInput{
ThingName: aws.String(thingName),
Payload: []byte(`{"state":{"reported":{"color":"green"}}}`),
})
require.NoError(t, err)
got, err := dataSvc.GetThingShadow(ctx, &iotdataplane.GetThingShadowInput{ThingName: aws.String(thingName)})
require.NoError(t, err)
require.NoError(t, json.Unmarshal(got.Payload, &shadow))
state := shadow["state"].(map[string]any)
assert.Equal(t, "blue", state["desired"].(map[string]any)["color"])
assert.Equal(t, "green", state["reported"].(map[string]any)["color"])
_, err = dataSvc.UpdateThingShadow(ctx, &iotdataplane.UpdateThingShadowInput{
ThingName: aws.String(thingName),
ShadowName: aws.String("settings"),
Payload: []byte(`{"state":{"desired":{"mode":"auto"}}}`),
})
require.NoError(t, err)
named, err := dataSvc.ListNamedShadowsForThing(ctx, &iotdataplane.ListNamedShadowsForThingInput{ThingName: aws.String(thingName)})
require.NoError(t, err)
assert.Contains(t, named.Results, "settings")
_, err = dataSvc.Publish(ctx, &iotdataplane.PublishInput{Topic: aws.String("devices/" + thingName + "/events"), Payload: []byte("payload")})
require.NoError(t, err)
_, err = dataSvc.DeleteThingShadow(ctx, &iotdataplane.DeleteThingShadowInput{ThingName: aws.String(thingName), ShadowName: aws.String("settings")})
require.NoError(t, err)
_, err = dataSvc.DeleteThingShadow(ctx, &iotdataplane.DeleteThingShadowInput{ThingName: aws.String(thingName)})
require.NoError(t, err)
})
t.Run("TopicRuleCrudAndRuleActions", func(t *testing.T) {
dataSvc := testutil.IoTDataClient()
dynamoSvc := testutil.DynamoDBClient()
kinesisSvc := testutil.KinesisClient()
s3Svc := testutil.S3Client()
sqsSvc := testutil.SQSClient()
ruleName := "go-iot-topic-rule"
bucketName := "go-iot-rule-bucket"
dynamoTable := "go-iot-rule-table"
streamName := "go-iot-rule-stream"
objectKey := "rules/output.json"
queueName := "go-iot-rule-queue"
_, _ = dynamoSvc.DeleteTable(ctx, &dynamodb.DeleteTableInput{TableName: aws.String(dynamoTable)})
_, _ = kinesisSvc.DeleteStream(ctx, &kinesis.DeleteStreamInput{StreamName: aws.String(streamName)})
_, _ = s3Svc.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: aws.String(bucketName), Key: aws.String(objectKey)})
_, _ = s3Svc.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: aws.String(bucketName)})
_, err := dynamoSvc.CreateTable(ctx, &dynamodb.CreateTableInput{
TableName: aws.String(dynamoTable),
BillingMode: ddbtypes.BillingModePayPerRequest,
AttributeDefinitions: []ddbtypes.AttributeDefinition{
{AttributeName: aws.String("pk"), AttributeType: ddbtypes.ScalarAttributeTypeS},
{AttributeName: aws.String("sk"), AttributeType: ddbtypes.ScalarAttributeTypeS},
},
KeySchema: []ddbtypes.KeySchemaElement{
{AttributeName: aws.String("pk"), KeyType: ddbtypes.KeyTypeHash},
{AttributeName: aws.String("sk"), KeyType: ddbtypes.KeyTypeRange},
},
})
require.NoError(t, err)
defer dynamoSvc.DeleteTable(ctx, &dynamodb.DeleteTableInput{TableName: aws.String(dynamoTable)})
_, err = kinesisSvc.CreateStream(ctx, &kinesis.CreateStreamInput{StreamName: aws.String(streamName), ShardCount: aws.Int32(1)})
require.NoError(t, err)
defer kinesisSvc.DeleteStream(ctx, &kinesis.DeleteStreamInput{StreamName: aws.String(streamName)})
_, err = s3Svc.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: aws.String(bucketName)})
require.NoError(t, err)
defer s3Svc.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: aws.String(bucketName), Key: aws.String(objectKey)})
defer s3Svc.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: aws.String(bucketName)})
queue, err := sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(queueName)})
require.NoError(t, err)
queueURL := aws.ToString(queue.QueueUrl)
defer sqsSvc.DeleteQueue(ctx, &sqs.DeleteQueueInput{QueueUrl: aws.String(queueURL)})
defer svc.DeleteTopicRule(ctx, &iot.DeleteTopicRuleInput{RuleName: aws.String(ruleName)})
payload := []byte(`{"pk":"iot#1","sk":"event","message":"go-rule-payload"}`)
_, err = svc.CreateTopicRule(ctx, &iot.CreateTopicRuleInput{
RuleName: aws.String(ruleName),
TopicRulePayload: &iottypes.TopicRulePayload{
Sql: aws.String("SELECT * FROM 'devices/go-iot/rules'"),
Description: aws.String("go topic rule"),
RuleDisabled: aws.Bool(false),
Actions: []iottypes.Action{
{Sqs: &iottypes.SqsAction{
RoleArn: aws.String("arn:aws:iam::000000000000:role/iot-rule-role"),
QueueUrl: aws.String(queueURL),
UseBase64: aws.Bool(false),
}},
{S3: &iottypes.S3Action{
RoleArn: aws.String("arn:aws:iam::000000000000:role/iot-rule-role"),
BucketName: aws.String(bucketName),
Key: aws.String(objectKey),
}},
{Kinesis: &iottypes.KinesisAction{
RoleArn: aws.String("arn:aws:iam::000000000000:role/iot-rule-role"),
StreamName: aws.String(streamName),
PartitionKey: aws.String("iot-rule"),
}},
{DynamoDBv2: &iottypes.DynamoDBv2Action{
RoleArn: aws.String("arn:aws:iam::000000000000:role/iot-rule-role"),
PutItem: &iottypes.PutItemInput{TableName: aws.String(dynamoTable)},
}},
},
},
})
require.NoError(t, err)
got, err := svc.GetTopicRule(ctx, &iot.GetTopicRuleInput{RuleName: aws.String(ruleName)})
require.NoError(t, err)
require.NotNil(t, got.Rule)
assert.Equal(t, ruleName, aws.ToString(got.Rule.RuleName))
assert.Equal(t, queueURL, aws.ToString(got.Rule.Actions[0].Sqs.QueueUrl))
assert.Equal(t, bucketName, aws.ToString(got.Rule.Actions[1].S3.BucketName))
assert.Equal(t, streamName, aws.ToString(got.Rule.Actions[2].Kinesis.StreamName))
assert.Equal(t, dynamoTable, aws.ToString(got.Rule.Actions[3].DynamoDBv2.PutItem.TableName))
_, err = svc.DisableTopicRule(ctx, &iot.DisableTopicRuleInput{RuleName: aws.String(ruleName)})
require.NoError(t, err)
listed, err := svc.ListTopicRules(ctx, &iot.ListTopicRulesInput{})
require.NoError(t, err)
foundDisabled := false
for _, rule := range listed.Rules {
if aws.ToString(rule.RuleName) == ruleName && aws.ToBool(rule.RuleDisabled) {
foundDisabled = true
}
}
assert.True(t, foundDisabled)
_, err = svc.EnableTopicRule(ctx, &iot.EnableTopicRuleInput{RuleName: aws.String(ruleName)})
require.NoError(t, err)
_, err = dataSvc.Publish(ctx, &iotdataplane.PublishInput{Topic: aws.String("devices/go-iot/rules"), Payload: payload})
require.NoError(t, err)
received, err := sqsSvc.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{QueueUrl: aws.String(queueURL), MaxNumberOfMessages: 1})
require.NoError(t, err)
require.Len(t, received.Messages, 1)
assert.Equal(t, string(payload), aws.ToString(received.Messages[0].Body))
object, err := s3Svc.GetObject(ctx, &s3.GetObjectInput{Bucket: aws.String(bucketName), Key: aws.String(objectKey)})
require.NoError(t, err)
defer object.Body.Close()
body, err := io.ReadAll(object.Body)
require.NoError(t, err)
assert.Equal(t, string(payload), string(body))
dynamoItem, err := dynamoSvc.GetItem(ctx, &dynamodb.GetItemInput{
TableName: aws.String(dynamoTable),
Key: map[string]ddbtypes.AttributeValue{
"pk": &ddbtypes.AttributeValueMemberS{Value: "iot#1"},
"sk": &ddbtypes.AttributeValueMemberS{Value: "event"},
},
})
require.NoError(t, err)
message, ok := dynamoItem.Item["message"].(*ddbtypes.AttributeValueMemberS)
require.True(t, ok)
assert.Equal(t, "go-rule-payload", message.Value)
stream, err := kinesisSvc.DescribeStream(ctx, &kinesis.DescribeStreamInput{StreamName: aws.String(streamName)})
require.NoError(t, err)
iterator, err := kinesisSvc.GetShardIterator(ctx, &kinesis.GetShardIteratorInput{
StreamName: aws.String(streamName),
ShardId: stream.StreamDescription.Shards[0].ShardId,
ShardIteratorType: kinesistypes.ShardIteratorTypeTrimHorizon,
})
require.NoError(t, err)
records, err := kinesisSvc.GetRecords(ctx, &kinesis.GetRecordsInput{ShardIterator: iterator.ShardIterator})
require.NoError(t, err)
require.NotEmpty(t, records.Records)
assert.Equal(t, payload, records.Records[0].Data)
})
t.Run("ThingTypesGroupsAndJobs", func(t *testing.T) {
jobsSvc := testutil.IoTJobsDataClient()
thingType := "go-iot-type"
thingName := "go-iot-typed-thing"
groupName := "go-iot-group"
jobID := "go-iot-job"
_, _ = svc.DeleteThing(ctx, &iot.DeleteThingInput{ThingName: aws.String(thingName)})
_, _ = svc.DeleteThingGroup(ctx, &iot.DeleteThingGroupInput{ThingGroupName: aws.String(groupName)})
_, _ = svc.DeprecateThingType(ctx, &iot.DeprecateThingTypeInput{ThingTypeName: aws.String(thingType)})
_, _ = svc.DeleteThingType(ctx, &iot.DeleteThingTypeInput{ThingTypeName: aws.String(thingType)})
jobsEndpoint, err := svc.DescribeEndpoint(ctx, &iot.DescribeEndpointInput{EndpointType: aws.String("iot:Jobs")})
require.NoError(t, err)
assert.NotEmpty(t, aws.ToString(jobsEndpoint.EndpointAddress))
createdType, err := svc.CreateThingType(ctx, &iot.CreateThingTypeInput{
ThingTypeName: aws.String(thingType),
ThingTypeProperties: &iottypes.ThingTypeProperties{
ThingTypeDescription: aws.String("go type"),
SearchableAttributes: []string{"model"},
},
})
require.NoError(t, err)
assert.Equal(t, thingType, aws.ToString(createdType.ThingTypeName))
describedType, err := svc.DescribeThingType(ctx, &iot.DescribeThingTypeInput{ThingTypeName: aws.String(thingType)})
require.NoError(t, err)
assert.Equal(t, "go type", aws.ToString(describedType.ThingTypeProperties.ThingTypeDescription))
_, err = svc.UpdateThingType(ctx, &iot.UpdateThingTypeInput{
ThingTypeName: aws.String(thingType),
ThingTypeProperties: &iottypes.ThingTypeProperties{
ThingTypeDescription: aws.String("go type updated"),
SearchableAttributes: []string{"model", "fw"},
},
})
require.NoError(t, err)
createdThing, err := svc.CreateThing(ctx, &iot.CreateThingInput{
ThingName: aws.String(thingName),
ThingTypeName: aws.String(thingType),
AttributePayload: &iottypes.AttributePayload{
Attributes: map[string]string{"model": "g1"},
},
})
require.NoError(t, err)
thingArn := aws.ToString(createdThing.ThingArn)
describedThing, err := svc.DescribeThing(ctx, &iot.DescribeThingInput{ThingName: aws.String(thingName)})
require.NoError(t, err)
assert.Equal(t, thingType, aws.ToString(describedThing.ThingTypeName))
createdGroup, err := svc.CreateThingGroup(ctx, &iot.CreateThingGroupInput{
ThingGroupName: aws.String(groupName),
ThingGroupProperties: &iottypes.ThingGroupProperties{
ThingGroupDescription: aws.String("go group"),
AttributePayload: &iottypes.AttributePayload{Attributes: map[string]string{"fleet": "go"}},
},
})
require.NoError(t, err)
assert.Equal(t, groupName, aws.ToString(createdGroup.ThingGroupName))
_, err = svc.AddThingToThingGroup(ctx, &iot.AddThingToThingGroupInput{ThingGroupName: aws.String(groupName), ThingName: aws.String(thingName)})
require.NoError(t, err)
thingsInGroup, err := svc.ListThingsInThingGroup(ctx, &iot.ListThingsInThingGroupInput{ThingGroupName: aws.String(groupName)})
require.NoError(t, err)
assert.Contains(t, thingsInGroup.Things, thingName)
jobDoc := `{"operation":"reboot"}`
createdJob, err := svc.CreateJob(ctx, &iot.CreateJobInput{JobId: aws.String(jobID), Targets: []string{thingArn}, Document: aws.String(jobDoc), Description: aws.String("go job")})
require.NoError(t, err)
assert.Equal(t, jobID, aws.ToString(createdJob.JobId))
pending, err := jobsSvc.GetPendingJobExecutions(ctx, &jobsdata.GetPendingJobExecutionsInput{ThingName: aws.String(thingName)})
require.NoError(t, err)
require.NotEmpty(t, pending.QueuedJobs)
started, err := jobsSvc.StartNextPendingJobExecution(ctx, &jobsdata.StartNextPendingJobExecutionInput{ThingName: aws.String(thingName), StatusDetails: map[string]string{"phase": "download"}})
require.NoError(t, err)
assert.Equal(t, jobsdatatypes.JobExecutionStatusInProgress, started.Execution.Status)
updated, err := jobsSvc.UpdateJobExecution(ctx, &jobsdata.UpdateJobExecutionInput{
ThingName: aws.String(thingName),
JobId: aws.String(jobID),
Status: jobsdatatypes.JobExecutionStatusSucceeded,
ExpectedVersion: aws.Int64(2),
IncludeJobExecutionState: aws.Bool(true),
IncludeJobDocument: aws.Bool(true),
})
require.NoError(t, err)
assert.Equal(t, jobsdatatypes.JobExecutionStatusSucceeded, updated.ExecutionState.Status)
assert.Contains(t, aws.ToString(updated.JobDocument), "reboot")
_, err = svc.RemoveThingFromThingGroup(ctx, &iot.RemoveThingFromThingGroupInput{ThingGroupName: aws.String(groupName), ThingName: aws.String(thingName)})
require.NoError(t, err)
_, err = svc.DeleteThingGroup(ctx, &iot.DeleteThingGroupInput{ThingGroupName: aws.String(groupName)})
require.NoError(t, err)
_, err = svc.DeleteThing(ctx, &iot.DeleteThingInput{ThingName: aws.String(thingName)})
require.NoError(t, err)
_, err = svc.DeprecateThingType(ctx, &iot.DeprecateThingTypeInput{ThingTypeName: aws.String(thingType)})
require.NoError(t, err)
_, err = svc.DeleteThingType(ctx, &iot.DeleteThingTypeInput{ThingTypeName: aws.String(thingType)})
require.NoError(t, err)
})
t.Run("MqttConnectPublishSubscribe", func(t *testing.T) {
topic := "devices/go-iot-mqtt/events"
payload := []byte("go-mqtt")
subscriber := mqttConnect(t, "go-iot-mqtt-sub")
defer subscriber.Close()
mqttSubscribe(t, subscriber, topic)
publisher := mqttConnect(t, "go-iot-mqtt-pub")
mqttPublish(t, publisher, topic, payload)
publisher.Close()
receivedTopic, receivedPayload := mqttReadPublish(t, subscriber)
assert.Equal(t, topic, receivedTopic)
assert.Equal(t, payload, receivedPayload)
})
t.Run("IoTDataConnectionApis", func(t *testing.T) {
dataSvc := testutil.IoTDataClient()
clientID := "go-iot-connection"
topic := "devices/go-iot-connection/direct"
payload := []byte("go-direct")
client := mqttConnect(t, clientID)
defer client.Close()
mqttSubscribe(t, client, topic)
connection, err := dataSvc.GetConnection(ctx, &iotdataplane.GetConnectionInput{
ClientId: aws.String(clientID),
IncludeSocketInformation: true,
})
require.NoError(t, err)
assert.Equal(t, clientID, aws.ToString(connection.ClientId))
assert.True(t, connection.Connected)
assert.NotEmpty(t, aws.ToString(connection.SourceIp))
subscriptions, err := dataSvc.ListSubscriptions(ctx, &iotdataplane.ListSubscriptionsInput{ClientId: aws.String(clientID)})
require.NoError(t, err)
require.NotEmpty(t, subscriptions.Subscriptions)
assert.Equal(t, topic, aws.ToString(subscriptions.Subscriptions[0].TopicFilter))
direct, err := dataSvc.SendDirectMessage(ctx, &iotdataplane.SendDirectMessageInput{
ClientId: aws.String(clientID),
Topic: aws.String(topic),
Payload: payload,
})
require.NoError(t, err)
assert.NotEmpty(t, aws.ToString(direct.TraceId))
receivedTopic, receivedPayload := mqttReadPublish(t, client)
assert.Equal(t, topic, receivedTopic)
assert.Equal(t, payload, receivedPayload)
})
t.Run("Mqtt5Connect", func(t *testing.T) {
client := mqtt5Connect(t, "go-iot-mqtt5")
client.Close()
})
t.Run("MqttShadowReservedTopics", func(t *testing.T) {
thingName := "go-iot-shadow"
subscriber := mqttConnect(t, "go-iot-shadow-sub")
defer subscriber.Close()
mqttSubscribe(t, subscriber, "$aws/things/"+thingName+"/shadow/update/accepted")
mqttSubscribe(t, subscriber, "$aws/things/"+thingName+"/shadow/get/accepted")
mqttSubscribe(t, subscriber, "$aws/things/"+thingName+"/shadow/delete/accepted")
publisher := mqttConnect(t, "go-iot-shadow-pub")
defer publisher.Close()
mqttPublish(t, publisher, "$aws/things/"+thingName+"/shadow/update", []byte(`{"state":{"desired":{"color":"blue"}},"clientToken":"update-token"}`))
topic, payload := mqttReadPublish(t, subscriber)
assert.Equal(t, "$aws/things/"+thingName+"/shadow/update/accepted", topic)
var accepted map[string]any
require.NoError(t, json.Unmarshal(payload, &accepted))
assert.Equal(t, "update-token", accepted["clientToken"])
mqttPublish(t, publisher, "$aws/things/"+thingName+"/shadow/get", []byte(`{"clientToken":"get-token"}`))
topic, payload = mqttReadPublish(t, subscriber)
assert.Equal(t, "$aws/things/"+thingName+"/shadow/get/accepted", topic)
var got map[string]any
require.NoError(t, json.Unmarshal(payload, &got))
assert.Equal(t, "get-token", got["clientToken"])
mqttPublish(t, publisher, "$aws/things/"+thingName+"/shadow/delete", []byte(`{"clientToken":"delete-token"}`))
topic, payload = mqttReadPublish(t, subscriber)
assert.Equal(t, "$aws/things/"+thingName+"/shadow/delete/accepted", topic)
var deleted map[string]any
require.NoError(t, json.Unmarshal(payload, &deleted))
assert.Equal(t, "delete-token", deleted["clientToken"])
})
}
func tagsByKey(tags []iottypes.Tag) map[string]string {
result := map[string]string{}
for _, tag := range tags {
result[aws.ToString(tag.Key)] = aws.ToString(tag.Value)
}
return result
}
func mqttConnect(t *testing.T, clientID string) net.Conn {
t.Helper()
conn, err := net.DialTimeout("tcp", "floci:1883", 5*time.Second)
require.NoError(t, err)
require.NoError(t, conn.SetDeadline(time.Now().Add(5*time.Second)))
body := append(mqttUTF8("MQTT"), []byte{4, 2, 0, 60}...)
body = append(body, mqttUTF8(clientID)...)
_, err = conn.Write(append([]byte{0x10}, append(mqttRemainingLength(len(body)), body...)...))
require.NoError(t, err)
packet := mqttReadPacket(t, conn)
require.Equal(t, []byte{0x20, 0x02, 0x00, 0x00}, packet)
return conn
}
func mqtt5Connect(t *testing.T, clientID string) net.Conn {
t.Helper()
conn, err := net.DialTimeout("tcp", "floci:1883", 5*time.Second)
require.NoError(t, err)
require.NoError(t, conn.SetDeadline(time.Now().Add(5*time.Second)))
body := append(mqttUTF8("MQTT"), []byte{5, 2, 0, 60, 0}...)
body = append(body, mqttUTF8(clientID)...)
_, err = conn.Write(append([]byte{0x10}, append(mqttRemainingLength(len(body)), body...)...))
require.NoError(t, err)
packet := mqttReadPacket(t, conn)
mqttAssertV5Connack(t, packet)
return conn
}
func mqttAssertV5Connack(t *testing.T, packet []byte) {
t.Helper()
require.Equal(t, byte(0x20), packet[0])
index := 1
for packet[index]&0x80 != 0 {
index++
}
index++
require.Equal(t, byte(0x00), packet[index])
require.Equal(t, byte(0x00), packet[index+1])
}
func mqttSubscribe(t *testing.T, conn net.Conn, topic string) {
t.Helper()
body := append([]byte{0, 1}, mqttUTF8(topic)...)
body = append(body, 0)
_, err := conn.Write(append([]byte{0x82}, append(mqttRemainingLength(len(body)), body...)...))
require.NoError(t, err)
require.Equal(t, []byte{0x90, 0x03, 0x00, 0x01, 0x00}, mqttReadPacket(t, conn))
}
func mqttPublish(t *testing.T, conn net.Conn, topic string, payload []byte) {
t.Helper()
body := append(mqttUTF8(topic), payload...)
_, err := conn.Write(append([]byte{0x30}, append(mqttRemainingLength(len(body)), body...)...))
require.NoError(t, err)
}
func mqttReadPublish(t *testing.T, conn net.Conn) (string, []byte) {
t.Helper()
packet := mqttReadPacket(t, conn)
require.Equal(t, byte(0x30), packet[0]&0xf0)
index := 1
for packet[index]&0x80 != 0 {
index++
}
index++
topicLength := int(packet[index])<<8 | int(packet[index+1])
index += 2
topic := string(packet[index : index+topicLength])
index += topicLength
return topic, packet[index:]
}
func mqttReadPacket(t *testing.T, conn net.Conn) []byte {
t.Helper()
fixed := make([]byte, 1)
_, err := io.ReadFull(conn, fixed)
require.NoError(t, err)
packet := append([]byte{}, fixed...)
remaining := 0
multiplier := 1
for {
encoded := make([]byte, 1)
_, err = io.ReadFull(conn, encoded)
require.NoError(t, err)
packet = append(packet, encoded[0])
remaining += int(encoded[0]&127) * multiplier
multiplier *= 128
if encoded[0]&128 == 0 {
break
}
}
body := make([]byte, remaining)
_, err = io.ReadFull(conn, body)
require.NoError(t, err)
return append(packet, body...)
}
func mqttUTF8(value string) []byte {
bytes := []byte(value)
return append([]byte{byte(len(bytes) >> 8), byte(len(bytes))}, bytes...)
}
func mqttRemainingLength(length int) []byte {
var encoded []byte
value := length
for {
digit := byte(value % 128)
value /= 128
if value > 0 {
digit |= 128
}
encoded = append(encoded, digit)
if value == 0 {
return encoded
}
}
}
@@ -0,0 +1,160 @@
package tests
import (
"context"
"testing"
"time"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/kinesis"
kinesistypes "github.com/aws/aws-sdk-go-v2/service/kinesis/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestKinesis(t *testing.T) {
ctx := context.Background()
svc := testutil.KinesisClient()
streamName := "go-test-stream"
t.Cleanup(func() {
svc.DeleteStream(ctx, &kinesis.DeleteStreamInput{StreamName: aws.String(streamName)})
})
t.Run("CreateStream", func(t *testing.T) {
_, err := svc.CreateStream(ctx, &kinesis.CreateStreamInput{
StreamName: aws.String(streamName),
ShardCount: aws.Int32(1),
})
require.NoError(t, err)
})
t.Run("ListStreams", func(t *testing.T) {
r, err := svc.ListStreams(ctx, &kinesis.ListStreamsInput{})
require.NoError(t, err)
assert.NotEmpty(t, r.StreamNames)
})
var shardID string
var streamARN string
t.Run("DescribeStream", func(t *testing.T) {
r, err := svc.DescribeStream(ctx, &kinesis.DescribeStreamInput{StreamName: aws.String(streamName)})
require.NoError(t, err)
require.NotNil(t, r.StreamDescription)
assert.Equal(t, kinesistypes.StreamStatusActive, r.StreamDescription.StreamStatus)
require.NotEmpty(t, r.StreamDescription.Shards)
shardID = aws.ToString(r.StreamDescription.Shards[0].ShardId)
require.NotEmpty(t, shardID)
streamARN = aws.ToString(r.StreamDescription.StreamARN)
require.NotEmpty(t, streamARN)
})
t.Run("PutRecord", func(t *testing.T) {
r, err := svc.PutRecord(ctx, &kinesis.PutRecordInput{
StreamName: aws.String(streamName),
Data: []byte(`{"event":"go-test"}`),
PartitionKey: aws.String("pk1"),
})
require.NoError(t, err)
assert.NotEmpty(t, aws.ToString(r.ShardId))
})
t.Run("GetShardIterator", func(t *testing.T) {
if shardID == "" {
t.Skip("No shard ID from DescribeStream")
}
r, err := svc.GetShardIterator(ctx, &kinesis.GetShardIteratorInput{
StreamName: aws.String(streamName),
ShardId: aws.String(shardID),
ShardIteratorType: kinesistypes.ShardIteratorTypeTrimHorizon,
})
require.NoError(t, err)
assert.NotEmpty(t, aws.ToString(r.ShardIterator))
})
t.Run("GetRecords", func(t *testing.T) {
if shardID == "" {
t.Skip("No shard ID from DescribeStream")
}
// Get fresh iterator
iter, err := svc.GetShardIterator(ctx, &kinesis.GetShardIteratorInput{
StreamName: aws.String(streamName),
ShardId: aws.String(shardID),
ShardIteratorType: kinesistypes.ShardIteratorTypeTrimHorizon,
})
require.NoError(t, err)
r, err := svc.GetRecords(ctx, &kinesis.GetRecordsInput{
ShardIterator: iter.ShardIterator,
Limit: aws.Int32(10),
})
require.NoError(t, err)
assert.NotEmpty(t, r.Records)
})
var consumerARN string
t.Run("RegisterStreamConsumer", func(t *testing.T) {
if streamARN == "" {
t.Skip("No stream ARN from DescribeStream")
}
r, err := svc.RegisterStreamConsumer(ctx, &kinesis.RegisterStreamConsumerInput{
StreamARN: aws.String(streamARN),
ConsumerName: aws.String("go-test-consumer"),
})
require.NoError(t, err)
require.NotNil(t, r.Consumer)
consumerARN = aws.ToString(r.Consumer.ConsumerARN)
assert.NotEmpty(t, consumerARN)
})
t.Run("DescribeStreamConsumer", func(t *testing.T) {
if consumerARN == "" {
t.Skip("No consumer ARN from RegisterStreamConsumer")
}
r, err := svc.DescribeStreamConsumer(ctx, &kinesis.DescribeStreamConsumerInput{
ConsumerARN: aws.String(consumerARN),
})
require.NoError(t, err)
assert.Equal(t, kinesistypes.ConsumerStatusActive, r.ConsumerDescription.ConsumerStatus)
assert.Equal(t, "go-test-consumer", aws.ToString(r.ConsumerDescription.ConsumerName))
})
t.Run("SubscribeToShard", func(t *testing.T) {
if consumerARN == "" || shardID == "" {
t.Skip("No consumerARN or shardID")
}
subCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
output, err := svc.SubscribeToShard(subCtx, &kinesis.SubscribeToShardInput{
ConsumerARN: aws.String(consumerARN),
ShardId: aws.String(shardID),
StartingPosition: &kinesistypes.StartingPosition{
Type: kinesistypes.ShardIteratorTypeTrimHorizon,
},
})
require.NoError(t, err)
stream := output.GetStream()
defer stream.Close()
var gotEvent bool
for event := range stream.Events() {
if ev, ok := event.(*kinesistypes.SubscribeToShardEventStreamMemberSubscribeToShardEvent); ok {
gotEvent = true
assert.NotNil(t, ev.Value.Records)
}
}
require.NoError(t, stream.Err())
assert.True(t, gotEvent, "expected at least one SubscribeToShardEvent")
})
t.Run("DeleteStream", func(t *testing.T) {
_, err := svc.DeleteStream(ctx, &kinesis.DeleteStreamInput{StreamName: aws.String(streamName)})
require.NoError(t, err)
})
}
@@ -0,0 +1,76 @@
package tests
import (
"context"
"testing"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/kms"
kmstypes "github.com/aws/aws-sdk-go-v2/service/kms/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestKMS(t *testing.T) {
ctx := context.Background()
svc := testutil.KMSClient()
var keyID string
// Note: KMS keys cannot be deleted immediately, so we don't clean up
t.Run("CreateKey", func(t *testing.T) {
r, err := svc.CreateKey(ctx, &kms.CreateKeyInput{
Description: aws.String("go-test key"),
})
require.NoError(t, err)
keyID = aws.ToString(r.KeyMetadata.KeyId)
assert.NotEmpty(t, keyID)
})
t.Run("DescribeKey", func(t *testing.T) {
r, err := svc.DescribeKey(ctx, &kms.DescribeKeyInput{KeyId: aws.String(keyID)})
require.NoError(t, err)
assert.Equal(t, keyID, aws.ToString(r.KeyMetadata.KeyId))
})
t.Run("ListKeys", func(t *testing.T) {
r, err := svc.ListKeys(ctx, &kms.ListKeysInput{})
require.NoError(t, err)
assert.NotEmpty(t, r.Keys)
})
var ciphertext []byte
t.Run("Encrypt", func(t *testing.T) {
r, err := svc.Encrypt(ctx, &kms.EncryptInput{
KeyId: aws.String(keyID),
Plaintext: []byte("hello-go-test"),
})
require.NoError(t, err)
ciphertext = r.CiphertextBlob
assert.NotEmpty(t, ciphertext)
})
t.Run("Decrypt", func(t *testing.T) {
if len(ciphertext) == 0 {
t.Skip("No ciphertext from Encrypt")
}
r, err := svc.Decrypt(ctx, &kms.DecryptInput{
CiphertextBlob: ciphertext,
KeyId: aws.String(keyID),
})
require.NoError(t, err)
assert.Equal(t, "hello-go-test", string(r.Plaintext))
})
t.Run("GenerateDataKey", func(t *testing.T) {
r, err := svc.GenerateDataKey(ctx, &kms.GenerateDataKeyInput{
KeyId: aws.String(keyID),
KeySpec: kmstypes.DataKeySpecAes256,
})
require.NoError(t, err)
assert.Len(t, r.Plaintext, 32)
})
}
@@ -0,0 +1,130 @@
package tests
import (
"context"
"encoding/json"
"errors"
"net"
"testing"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/lambda"
lambdatypes "github.com/aws/aws-sdk-go-v2/service/lambda/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLambda(t *testing.T) {
ctx := context.Background()
svc := testutil.LambdaClient()
funcName := "go-test-func"
roleARN := "arn:aws:iam::000000000000:role/test-role"
t.Cleanup(func() {
svc.DeleteFunction(ctx, &lambda.DeleteFunctionInput{FunctionName: aws.String(funcName)})
})
t.Run("CreateFunction", func(t *testing.T) {
_, err := svc.CreateFunction(ctx, &lambda.CreateFunctionInput{
FunctionName: aws.String(funcName),
Runtime: lambdatypes.RuntimeNodejs18x,
Role: aws.String(roleARN),
Handler: aws.String("index.handler"),
Code: &lambdatypes.FunctionCode{ZipFile: testutil.MinimalZip()},
})
require.NoError(t, err)
})
t.Run("GetFunction", func(t *testing.T) {
r, err := svc.GetFunction(ctx, &lambda.GetFunctionInput{FunctionName: aws.String(funcName)})
require.NoError(t, err)
assert.Equal(t, funcName, aws.ToString(r.Configuration.FunctionName))
})
t.Run("ListFunctions", func(t *testing.T) {
r, err := svc.ListFunctions(ctx, &lambda.ListFunctionsInput{})
require.NoError(t, err)
assert.NotEmpty(t, r.Functions)
})
t.Run("Invoke", func(t *testing.T) {
payload, _ := json.Marshal(map[string]string{"name": "GoTest"})
r, err := svc.Invoke(ctx, &lambda.InvokeInput{
FunctionName: aws.String(funcName),
Payload: payload,
})
if err != nil {
// Only skip on transport-level failures (timeout, connection refused).
// Let unexpected service errors fail the test.
var netErr net.Error
if errors.As(err, &netErr) {
t.Skipf("Lambda REQUEST_RESPONSE dispatch unavailable (transport error): %v", err)
}
require.NoError(t, err)
}
assert.Equal(t, int32(200), r.StatusCode)
assert.Nil(t, r.FunctionError)
})
t.Run("DeleteFunction", func(t *testing.T) {
_, err := svc.DeleteFunction(ctx, &lambda.DeleteFunctionInput{FunctionName: aws.String(funcName)})
require.NoError(t, err)
})
}
func TestLambdaImageConfigWorkingDirectory(t *testing.T) {
ctx := context.Background()
svc := testutil.LambdaClient()
roleARN := "arn:aws:iam::000000000000:role/test-role"
imageURI := "000000000000.dkr.ecr.us-east-1.amazonaws.com/fake-repo:latest"
t.Run("RoundTripsWorkingDirectoryOnCreate", func(t *testing.T) {
fnName := "go-test-imgwd-create"
t.Cleanup(func() {
svc.DeleteFunction(ctx, &lambda.DeleteFunctionInput{FunctionName: aws.String(fnName)})
})
createResp, err := svc.CreateFunction(ctx, &lambda.CreateFunctionInput{
FunctionName: aws.String(fnName),
PackageType: lambdatypes.PackageTypeImage,
Role: aws.String(roleARN),
Code: &lambdatypes.FunctionCode{ImageUri: aws.String(imageURI)},
ImageConfig: &lambdatypes.ImageConfig{WorkingDirectory: aws.String("/app")},
})
require.NoError(t, err)
require.NotNil(t, createResp.ImageConfigResponse)
require.NotNil(t, createResp.ImageConfigResponse.ImageConfig)
assert.Equal(t, "/app", aws.ToString(createResp.ImageConfigResponse.ImageConfig.WorkingDirectory))
getResp, err := svc.GetFunctionConfiguration(ctx, &lambda.GetFunctionConfigurationInput{
FunctionName: aws.String(fnName),
})
require.NoError(t, err)
assert.Equal(t, "/app", aws.ToString(getResp.ImageConfigResponse.ImageConfig.WorkingDirectory))
})
t.Run("UpdatesWorkingDirectory", func(t *testing.T) {
fnName := "go-test-imgwd-update"
t.Cleanup(func() {
svc.DeleteFunction(ctx, &lambda.DeleteFunctionInput{FunctionName: aws.String(fnName)})
})
_, err := svc.CreateFunction(ctx, &lambda.CreateFunctionInput{
FunctionName: aws.String(fnName),
PackageType: lambdatypes.PackageTypeImage,
Role: aws.String(roleARN),
Code: &lambdatypes.FunctionCode{ImageUri: aws.String(imageURI)},
ImageConfig: &lambdatypes.ImageConfig{WorkingDirectory: aws.String("/initial")},
})
require.NoError(t, err)
updateResp, err := svc.UpdateFunctionConfiguration(ctx, &lambda.UpdateFunctionConfigurationInput{
FunctionName: aws.String(fnName),
ImageConfig: &lambdatypes.ImageConfig{WorkingDirectory: aws.String("/updated")},
})
require.NoError(t, err)
assert.Equal(t, "/updated", aws.ToString(updateResp.ImageConfigResponse.ImageConfig.WorkingDirectory))
})
}
@@ -0,0 +1,131 @@
package tests
import (
"context"
"fmt"
"testing"
"time"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/neptune"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNeptuneCluster(t *testing.T) {
ctx := context.Background()
svc := testutil.NeptuneClient()
clusterId := fmt.Sprintf("go-neptune-%d", time.Now().UnixMilli()%100000)
t.Cleanup(func() {
svc.DeleteDBCluster(ctx, &neptune.DeleteDBClusterInput{ //nolint:errcheck
DBClusterIdentifier: aws.String(clusterId),
SkipFinalSnapshot: aws.Bool(true),
})
})
t.Run("CreateDBCluster", func(t *testing.T) {
out, err := svc.CreateDBCluster(ctx, &neptune.CreateDBClusterInput{
DBClusterIdentifier: aws.String(clusterId),
Engine: aws.String("neptune"),
})
require.NoError(t, err)
require.NotNil(t, out.DBCluster)
assert.Equal(t, clusterId, aws.ToString(out.DBCluster.DBClusterIdentifier))
assert.Equal(t, "neptune", aws.ToString(out.DBCluster.Engine))
assert.Equal(t, "available", aws.ToString(out.DBCluster.Status))
assert.Contains(t, aws.ToString(out.DBCluster.DBClusterArn), "arn:aws:neptune:")
assert.Greater(t, aws.ToInt32(out.DBCluster.Port), int32(0))
})
t.Run("DescribeDBClusters", func(t *testing.T) {
out, err := svc.DescribeDBClusters(ctx, &neptune.DescribeDBClustersInput{
DBClusterIdentifier: aws.String(clusterId),
})
require.NoError(t, err)
require.Len(t, out.DBClusters, 1)
assert.Equal(t, clusterId, aws.ToString(out.DBClusters[0].DBClusterIdentifier))
})
t.Run("ModifyDBCluster", func(t *testing.T) {
out, err := svc.ModifyDBCluster(ctx, &neptune.ModifyDBClusterInput{
DBClusterIdentifier: aws.String(clusterId),
EnableIAMDatabaseAuthentication: aws.Bool(true),
})
require.NoError(t, err)
assert.True(t, aws.ToBool(out.DBCluster.IAMDatabaseAuthenticationEnabled))
})
t.Run("DeleteDBCluster", func(t *testing.T) {
_, err := svc.DeleteDBCluster(ctx, &neptune.DeleteDBClusterInput{
DBClusterIdentifier: aws.String(clusterId),
SkipFinalSnapshot: aws.Bool(true),
})
require.NoError(t, err)
})
}
func TestNeptuneInstance(t *testing.T) {
ctx := context.Background()
svc := testutil.NeptuneClient()
clusterId := fmt.Sprintf("go-neptune-cl-%d", time.Now().UnixMilli()%100000)
instanceId := fmt.Sprintf("go-neptune-inst-%d", time.Now().UnixMilli()%100000)
// Create cluster first
_, err := svc.CreateDBCluster(ctx, &neptune.CreateDBClusterInput{
DBClusterIdentifier: aws.String(clusterId),
Engine: aws.String("neptune"),
})
require.NoError(t, err, "setup: create Neptune cluster")
t.Cleanup(func() {
svc.DeleteDBInstance(ctx, &neptune.DeleteDBInstanceInput{ //nolint:errcheck
DBInstanceIdentifier: aws.String(instanceId),
})
svc.DeleteDBCluster(ctx, &neptune.DeleteDBClusterInput{ //nolint:errcheck
DBClusterIdentifier: aws.String(clusterId),
SkipFinalSnapshot: aws.Bool(true),
})
})
t.Run("CreateDBInstance", func(t *testing.T) {
out, err := svc.CreateDBInstance(ctx, &neptune.CreateDBInstanceInput{
DBInstanceIdentifier: aws.String(instanceId),
DBClusterIdentifier: aws.String(clusterId),
DBInstanceClass: aws.String("db.r5.large"),
Engine: aws.String("neptune"),
})
require.NoError(t, err)
require.NotNil(t, out.DBInstance)
assert.Equal(t, instanceId, aws.ToString(out.DBInstance.DBInstanceIdentifier))
assert.Equal(t, clusterId, aws.ToString(out.DBInstance.DBClusterIdentifier))
assert.Equal(t, "available", aws.ToString(out.DBInstance.DBInstanceStatus))
})
t.Run("DescribeDBInstances", func(t *testing.T) {
out, err := svc.DescribeDBInstances(ctx, &neptune.DescribeDBInstancesInput{
DBInstanceIdentifier: aws.String(instanceId),
})
require.NoError(t, err)
require.Len(t, out.DBInstances, 1)
assert.Equal(t, instanceId, aws.ToString(out.DBInstances[0].DBInstanceIdentifier))
})
t.Run("ModifyDBInstance", func(t *testing.T) {
out, err := svc.ModifyDBInstance(ctx, &neptune.ModifyDBInstanceInput{
DBInstanceIdentifier: aws.String(instanceId),
DBInstanceClass: aws.String("db.r5.xlarge"),
})
require.NoError(t, err)
assert.Equal(t, "db.r5.xlarge", aws.ToString(out.DBInstance.DBInstanceClass))
})
t.Run("DeleteDBInstance", func(t *testing.T) {
_, err := svc.DeleteDBInstance(ctx, &neptune.DeleteDBInstanceInput{
DBInstanceIdentifier: aws.String(instanceId),
})
require.NoError(t, err)
})
}
@@ -0,0 +1,450 @@
package tests
import (
"context"
"fmt"
"strings"
"testing"
"time"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/pipes"
pipestypes "github.com/aws/aws-sdk-go-v2/service/pipes/types"
"github.com/aws/aws-sdk-go-v2/service/sqs"
sqstypes "github.com/aws/aws-sdk-go-v2/service/sqs/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
pipesAccountID = "000000000000"
pipesRegion = "us-east-1"
pipesRoleArn = "arn:aws:iam::000000000000:role/pipe-role"
)
func sqsArn(queueName string) string {
return "arn:aws:sqs:" + pipesRegion + ":" + pipesAccountID + ":" + queueName
}
func TestPipes(t *testing.T) {
ctx := context.Background()
pipesSvc := testutil.PipesClient()
sqsSvc := testutil.SQSClient()
t.Run("CreatePipe", func(t *testing.T) {
pipeName := "go-test-create-pipe"
srcQueue := "go-pipe-src-create"
tgtQueue := "go-pipe-tgt-create"
sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(srcQueue)})
sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(tgtQueue)})
t.Cleanup(func() {
pipesSvc.DeletePipe(ctx, &pipes.DeletePipeInput{Name: aws.String(pipeName)})
cleanupQueue(ctx, sqsSvc, srcQueue)
cleanupQueue(ctx, sqsSvc, tgtQueue)
})
r, err := pipesSvc.CreatePipe(ctx, &pipes.CreatePipeInput{
Name: aws.String(pipeName),
Source: aws.String(sqsArn(srcQueue)),
Target: aws.String(sqsArn(tgtQueue)),
RoleArn: aws.String(pipesRoleArn),
DesiredState: pipestypes.RequestedPipeStateStopped,
})
require.NoError(t, err)
assert.Equal(t, pipestypes.PipeStateStopped, r.CurrentState)
assert.True(t, strings.Contains(aws.ToString(r.Arn), pipeName))
})
t.Run("DescribePipe", func(t *testing.T) {
pipeName := "go-test-describe-pipe"
srcQueue := "go-pipe-src-describe"
tgtQueue := "go-pipe-tgt-describe"
sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(srcQueue)})
sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(tgtQueue)})
t.Cleanup(func() {
pipesSvc.DeletePipe(ctx, &pipes.DeletePipeInput{Name: aws.String(pipeName)})
cleanupQueue(ctx, sqsSvc, srcQueue)
cleanupQueue(ctx, sqsSvc, tgtQueue)
})
pipesSvc.CreatePipe(ctx, &pipes.CreatePipeInput{
Name: aws.String(pipeName),
Source: aws.String(sqsArn(srcQueue)),
Target: aws.String(sqsArn(tgtQueue)),
RoleArn: aws.String(pipesRoleArn),
DesiredState: pipestypes.RequestedPipeStateStopped,
})
r, err := pipesSvc.DescribePipe(ctx, &pipes.DescribePipeInput{Name: aws.String(pipeName)})
require.NoError(t, err)
assert.Equal(t, pipeName, aws.ToString(r.Name))
assert.Equal(t, sqsArn(srcQueue), aws.ToString(r.Source))
assert.Equal(t, sqsArn(tgtQueue), aws.ToString(r.Target))
assert.Equal(t, pipestypes.PipeStateStopped, r.CurrentState)
})
t.Run("ListPipes", func(t *testing.T) {
pipeName := "go-test-list-pipe"
srcQueue := "go-pipe-src-list"
tgtQueue := "go-pipe-tgt-list"
sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(srcQueue)})
sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(tgtQueue)})
t.Cleanup(func() {
pipesSvc.DeletePipe(ctx, &pipes.DeletePipeInput{Name: aws.String(pipeName)})
cleanupQueue(ctx, sqsSvc, srcQueue)
cleanupQueue(ctx, sqsSvc, tgtQueue)
})
pipesSvc.CreatePipe(ctx, &pipes.CreatePipeInput{
Name: aws.String(pipeName),
Source: aws.String(sqsArn(srcQueue)),
Target: aws.String(sqsArn(tgtQueue)),
RoleArn: aws.String(pipesRoleArn),
DesiredState: pipestypes.RequestedPipeStateStopped,
})
r, err := pipesSvc.ListPipes(ctx, &pipes.ListPipesInput{})
require.NoError(t, err)
found := false
for _, p := range r.Pipes {
if aws.ToString(p.Name) == pipeName {
found = true
break
}
}
assert.True(t, found, "pipe should appear in list")
})
t.Run("UpdatePipe", func(t *testing.T) {
pipeName := "go-test-update-pipe"
srcQueue := "go-pipe-src-update"
tgtQueue := "go-pipe-tgt-update"
sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(srcQueue)})
sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(tgtQueue)})
t.Cleanup(func() {
pipesSvc.DeletePipe(ctx, &pipes.DeletePipeInput{Name: aws.String(pipeName)})
cleanupQueue(ctx, sqsSvc, srcQueue)
cleanupQueue(ctx, sqsSvc, tgtQueue)
})
pipesSvc.CreatePipe(ctx, &pipes.CreatePipeInput{
Name: aws.String(pipeName),
Source: aws.String(sqsArn(srcQueue)),
Target: aws.String(sqsArn(tgtQueue)),
RoleArn: aws.String(pipesRoleArn),
DesiredState: pipestypes.RequestedPipeStateStopped,
})
_, err := pipesSvc.UpdatePipe(ctx, &pipes.UpdatePipeInput{
Name: aws.String(pipeName),
RoleArn: aws.String(pipesRoleArn),
Description: aws.String("updated via SDK"),
DesiredState: pipestypes.RequestedPipeStateStopped,
})
require.NoError(t, err)
r, err := pipesSvc.DescribePipe(ctx, &pipes.DescribePipeInput{Name: aws.String(pipeName)})
require.NoError(t, err)
assert.Equal(t, "updated via SDK", aws.ToString(r.Description))
})
t.Run("DeletePipe", func(t *testing.T) {
pipeName := "go-test-delete-pipe"
srcQueue := "go-pipe-src-delete"
tgtQueue := "go-pipe-tgt-delete"
sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(srcQueue)})
sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(tgtQueue)})
t.Cleanup(func() {
cleanupQueue(ctx, sqsSvc, srcQueue)
cleanupQueue(ctx, sqsSvc, tgtQueue)
})
pipesSvc.CreatePipe(ctx, &pipes.CreatePipeInput{
Name: aws.String(pipeName),
Source: aws.String(sqsArn(srcQueue)),
Target: aws.String(sqsArn(tgtQueue)),
RoleArn: aws.String(pipesRoleArn),
DesiredState: pipestypes.RequestedPipeStateStopped,
})
_, err := pipesSvc.DeletePipe(ctx, &pipes.DeletePipeInput{Name: aws.String(pipeName)})
require.NoError(t, err)
_, err = pipesSvc.DescribePipe(ctx, &pipes.DescribePipeInput{Name: aws.String(pipeName)})
require.Error(t, err)
})
t.Run("DescribeNonExistentPipe", func(t *testing.T) {
_, err := pipesSvc.DescribePipe(ctx, &pipes.DescribePipeInput{Name: aws.String("nonexistent-pipe")})
require.Error(t, err)
})
t.Run("StartAndStopPipe", func(t *testing.T) {
pipeName := "go-test-startstop-pipe"
srcQueue := "go-pipe-src-startstop"
tgtQueue := "go-pipe-tgt-startstop"
sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(srcQueue)})
sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(tgtQueue)})
t.Cleanup(func() {
pipesSvc.DeletePipe(ctx, &pipes.DeletePipeInput{Name: aws.String(pipeName)})
cleanupQueue(ctx, sqsSvc, srcQueue)
cleanupQueue(ctx, sqsSvc, tgtQueue)
})
pipesSvc.CreatePipe(ctx, &pipes.CreatePipeInput{
Name: aws.String(pipeName),
Source: aws.String(sqsArn(srcQueue)),
Target: aws.String(sqsArn(tgtQueue)),
RoleArn: aws.String(pipesRoleArn),
DesiredState: pipestypes.RequestedPipeStateStopped,
})
startR, err := pipesSvc.StartPipe(ctx, &pipes.StartPipeInput{Name: aws.String(pipeName)})
require.NoError(t, err)
assert.Equal(t, pipestypes.PipeStateRunning, startR.CurrentState)
stopR, err := pipesSvc.StopPipe(ctx, &pipes.StopPipeInput{Name: aws.String(pipeName)})
require.NoError(t, err)
assert.Equal(t, pipestypes.PipeStateStopped, stopR.CurrentState)
})
t.Run("SQSToSQSForwarding", func(t *testing.T) {
pipeName := "go-test-fwd-pipe"
srcQueue := "go-pipe-src-fwd"
tgtQueue := "go-pipe-tgt-fwd"
srcResp, _ := sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(srcQueue)})
srcURL := aws.ToString(srcResp.QueueUrl)
tgtResp, _ := sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(tgtQueue)})
tgtURL := aws.ToString(tgtResp.QueueUrl)
t.Cleanup(func() {
pipesSvc.DeletePipe(ctx, &pipes.DeletePipeInput{Name: aws.String(pipeName)})
sqsSvc.DeleteQueue(ctx, &sqs.DeleteQueueInput{QueueUrl: aws.String(srcURL)})
sqsSvc.DeleteQueue(ctx, &sqs.DeleteQueueInput{QueueUrl: aws.String(tgtURL)})
})
pipesSvc.CreatePipe(ctx, &pipes.CreatePipeInput{
Name: aws.String(pipeName),
Source: aws.String(sqsArn(srcQueue)),
Target: aws.String(sqsArn(tgtQueue)),
RoleArn: aws.String(pipesRoleArn),
DesiredState: pipestypes.RequestedPipeStateRunning,
})
sqsSvc.SendMessage(ctx, &sqs.SendMessageInput{
QueueUrl: aws.String(srcURL),
MessageBody: aws.String("hello from pipes"),
})
found := false
for i := 0; i < 15; i++ {
time.Sleep(1 * time.Second)
r, err := sqsSvc.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
QueueUrl: aws.String(tgtURL),
MaxNumberOfMessages: 1,
WaitTimeSeconds: 1,
})
if err == nil && len(r.Messages) > 0 && strings.Contains(aws.ToString(r.Messages[0].Body), "hello from pipes") {
found = true
break
}
}
assert.True(t, found, "target queue should receive forwarded message")
})
t.Run("FilterCriteriaFiltersMessages", func(t *testing.T) {
pipeName := "go-test-filter-pipe"
srcQueue := "go-pipe-src-filter"
tgtQueue := "go-pipe-tgt-filter"
srcResp, _ := sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(srcQueue)})
srcURL := aws.ToString(srcResp.QueueUrl)
tgtResp, _ := sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(tgtQueue)})
tgtURL := aws.ToString(tgtResp.QueueUrl)
t.Cleanup(func() {
pipesSvc.DeletePipe(ctx, &pipes.DeletePipeInput{Name: aws.String(pipeName)})
sqsSvc.DeleteQueue(ctx, &sqs.DeleteQueueInput{QueueUrl: aws.String(srcURL)})
sqsSvc.DeleteQueue(ctx, &sqs.DeleteQueueInput{QueueUrl: aws.String(tgtURL)})
})
pipesSvc.CreatePipe(ctx, &pipes.CreatePipeInput{
Name: aws.String(pipeName),
Source: aws.String(sqsArn(srcQueue)),
Target: aws.String(sqsArn(tgtQueue)),
RoleArn: aws.String(pipesRoleArn),
DesiredState: pipestypes.RequestedPipeStateRunning,
SourceParameters: &pipestypes.PipeSourceParameters{
FilterCriteria: &pipestypes.FilterCriteria{
Filters: []pipestypes.Filter{
{Pattern: aws.String(`{"body": {"status": ["active"]}}`)},
},
},
},
})
sqsSvc.SendMessage(ctx, &sqs.SendMessageInput{
QueueUrl: aws.String(srcURL),
MessageBody: aws.String(`{"status": "active", "id": "match-1"}`),
})
sqsSvc.SendMessage(ctx, &sqs.SendMessageInput{
QueueUrl: aws.String(srcURL),
MessageBody: aws.String(`{"status": "inactive", "id": "no-match"}`),
})
found := false
for i := 0; i < 15; i++ {
time.Sleep(1 * time.Second)
r, err := sqsSvc.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
QueueUrl: aws.String(tgtURL),
MaxNumberOfMessages: 10,
WaitTimeSeconds: 1,
})
if err == nil && len(r.Messages) > 0 {
hasMatch := false
hasNoMatch := false
for _, m := range r.Messages {
if strings.Contains(aws.ToString(m.Body), "match-1") {
hasMatch = true
}
if strings.Contains(aws.ToString(m.Body), "no-match") {
hasNoMatch = true
}
}
if hasMatch {
assert.False(t, hasNoMatch, "non-matching message should not be forwarded")
found = true
break
}
}
}
assert.True(t, found, "target queue should receive matching message")
attrResp, err := sqsSvc.GetQueueAttributes(ctx, &sqs.GetQueueAttributesInput{
QueueUrl: aws.String(srcURL),
AttributeNames: []sqstypes.QueueAttributeName{sqstypes.QueueAttributeNameApproximateNumberOfMessages},
})
require.NoError(t, err)
assert.Equal(t, "0", attrResp.Attributes[string(sqstypes.QueueAttributeNameApproximateNumberOfMessages)])
})
t.Run("BatchSizeInSourceParameters", func(t *testing.T) {
pipeName := "go-test-batch-pipe"
srcQueue := "go-pipe-src-batch"
tgtQueue := "go-pipe-tgt-batch"
srcResp, _ := sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(srcQueue)})
srcURL := aws.ToString(srcResp.QueueUrl)
tgtResp, _ := sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(tgtQueue)})
tgtURL := aws.ToString(tgtResp.QueueUrl)
t.Cleanup(func() {
pipesSvc.DeletePipe(ctx, &pipes.DeletePipeInput{Name: aws.String(pipeName)})
sqsSvc.DeleteQueue(ctx, &sqs.DeleteQueueInput{QueueUrl: aws.String(srcURL)})
sqsSvc.DeleteQueue(ctx, &sqs.DeleteQueueInput{QueueUrl: aws.String(tgtURL)})
})
pipesSvc.CreatePipe(ctx, &pipes.CreatePipeInput{
Name: aws.String(pipeName),
Source: aws.String(sqsArn(srcQueue)),
Target: aws.String(sqsArn(tgtQueue)),
RoleArn: aws.String(pipesRoleArn),
DesiredState: pipestypes.RequestedPipeStateRunning,
SourceParameters: &pipestypes.PipeSourceParameters{
SqsQueueParameters: &pipestypes.PipeSourceSqsQueueParameters{
BatchSize: aws.Int32(1),
},
},
})
for i := 1; i <= 3; i++ {
sqsSvc.SendMessage(ctx, &sqs.SendMessageInput{
QueueUrl: aws.String(srcURL),
MessageBody: aws.String(fmt.Sprintf("batch-msg-%d", i)),
})
}
foundMessages := make(map[string]bool)
for i := 0; i < 20 && len(foundMessages) < 3; i++ {
time.Sleep(1 * time.Second)
r, err := sqsSvc.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
QueueUrl: aws.String(tgtURL),
MaxNumberOfMessages: 10,
WaitTimeSeconds: 1,
})
if err == nil {
for _, msg := range r.Messages {
for j := 1; j <= 3; j++ {
key := fmt.Sprintf("batch-msg-%d", j)
if strings.Contains(aws.ToString(msg.Body), key) {
foundMessages[key] = true
}
}
}
}
}
assert.Len(t, foundMessages, 3, "all 3 messages should arrive at target")
})
t.Run("StoppedPipeDoesNotForward", func(t *testing.T) {
pipeName := "go-test-nofwd-pipe"
srcQueue := "go-pipe-src-nofwd"
tgtQueue := "go-pipe-tgt-nofwd"
srcResp, _ := sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(srcQueue)})
srcURL := aws.ToString(srcResp.QueueUrl)
tgtResp, _ := sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(tgtQueue)})
tgtURL := aws.ToString(tgtResp.QueueUrl)
t.Cleanup(func() {
pipesSvc.DeletePipe(ctx, &pipes.DeletePipeInput{Name: aws.String(pipeName)})
sqsSvc.DeleteQueue(ctx, &sqs.DeleteQueueInput{QueueUrl: aws.String(srcURL)})
sqsSvc.DeleteQueue(ctx, &sqs.DeleteQueueInput{QueueUrl: aws.String(tgtURL)})
})
pipesSvc.CreatePipe(ctx, &pipes.CreatePipeInput{
Name: aws.String(pipeName),
Source: aws.String(sqsArn(srcQueue)),
Target: aws.String(sqsArn(tgtQueue)),
RoleArn: aws.String(pipesRoleArn),
DesiredState: pipestypes.RequestedPipeStateStopped,
})
sqsSvc.SendMessage(ctx, &sqs.SendMessageInput{
QueueUrl: aws.String(srcURL),
MessageBody: aws.String("should not forward"),
})
time.Sleep(3 * time.Second)
r, _ := sqsSvc.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
QueueUrl: aws.String(tgtURL),
MaxNumberOfMessages: 1,
WaitTimeSeconds: 1,
})
assert.Empty(t, r.Messages, "target queue should be empty")
})
}
func cleanupQueue(ctx context.Context, sqsSvc *sqs.Client, queueName string) {
r, err := sqsSvc.GetQueueUrl(ctx, &sqs.GetQueueUrlInput{QueueName: aws.String(queueName)})
if err == nil {
sqsSvc.DeleteQueue(ctx, &sqs.DeleteQueueInput{QueueUrl: r.QueueUrl})
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,223 @@
package tests
import (
"context"
"database/sql"
"fmt"
"testing"
"time"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/rds"
"github.com/aws/aws-sdk-go-v2/service/rds/types"
_ "github.com/lib/pq"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
rdsUsername = "admin"
rdsPassword = "secret123"
rdsDatabase = "app"
)
// TestRDSInstance covers management-plane CRUD for a DB instance (fix #567 — delete
// cleans up properly) and direct proxy connectivity (fix #503 — non-master pass-through).
func TestRDSInstance(t *testing.T) {
ctx := context.Background()
svc := testutil.RDSClient()
instanceId := fmt.Sprintf("go-rds-%d", time.Now().UnixMilli()%100000)
var proxyPort int32
t.Cleanup(func() {
svc.DeleteDBInstance(ctx, &rds.DeleteDBInstanceInput{ //nolint:errcheck
DBInstanceIdentifier: aws.String(instanceId),
SkipFinalSnapshot: aws.Bool(true),
})
})
t.Run("CreateDBInstance", func(t *testing.T) {
out, err := svc.CreateDBInstance(ctx, &rds.CreateDBInstanceInput{
DBInstanceIdentifier: aws.String(instanceId),
DBInstanceClass: aws.String("db.t3.micro"),
Engine: aws.String("postgres"),
MasterUsername: aws.String(rdsUsername),
MasterUserPassword: aws.String(rdsPassword),
DBName: aws.String(rdsDatabase),
AllocatedStorage: aws.Int32(20),
EnableIAMDatabaseAuthentication: aws.Bool(false),
})
require.NoError(t, err)
require.NotNil(t, out.DBInstance)
assert.Equal(t, instanceId, aws.ToString(out.DBInstance.DBInstanceIdentifier))
assert.Equal(t, "postgres", aws.ToString(out.DBInstance.Engine))
proxyPort = aws.ToInt32(out.DBInstance.Endpoint.Port)
assert.Greater(t, proxyPort, int32(0))
})
t.Run("DescribeDBInstance", func(t *testing.T) {
out, err := svc.DescribeDBInstances(ctx, &rds.DescribeDBInstancesInput{
DBInstanceIdentifier: aws.String(instanceId),
})
require.NoError(t, err)
require.Len(t, out.DBInstances, 1)
inst := out.DBInstances[0]
assert.Equal(t, instanceId, aws.ToString(inst.DBInstanceIdentifier))
assert.Equal(t, "postgres", aws.ToString(inst.Engine))
assert.Equal(t, rdsUsername, aws.ToString(inst.MasterUsername))
})
// Connect master user via the RDS proxy using plain password.
t.Run("ConnectMasterUser", func(t *testing.T) {
if proxyPort == 0 {
t.Skip("proxy port not set — CreateDBInstance step skipped")
}
conn := awaitPostgresConn(t, testutil.ProxyHost(), int(proxyPort), rdsUsername, rdsPassword, rdsDatabase)
defer conn.Close()
var result int
require.NoError(t, conn.QueryRowContext(ctx, "SELECT 1").Scan(&result))
assert.Equal(t, 1, result)
})
// Fix #503: non-master users must pass through to the backend without
// the proxy intercepting or rejecting their credentials.
t.Run("ConnectNonMasterUser", func(t *testing.T) {
if proxyPort == 0 {
t.Skip("proxy port not set — CreateDBInstance step skipped")
}
masterConn := awaitPostgresConn(t, testutil.ProxyHost(), int(proxyPort), rdsUsername, rdsPassword, rdsDatabase)
defer masterConn.Close()
_, err := masterConn.ExecContext(ctx,
"CREATE USER rds_testuser WITH PASSWORD 'testpass' LOGIN")
require.NoError(t, err)
userConn, err := openPostgresConn(testutil.ProxyHost(), int(proxyPort), "rds_testuser", "testpass", rdsDatabase)
require.NoError(t, err, "non-master user should connect via proxy pass-through")
defer userConn.Close()
var result int
require.NoError(t, userConn.QueryRowContext(ctx, "SELECT 1").Scan(&result))
assert.Equal(t, 1, result)
})
// Fix #567: deleting an instance must remove it from the describe list. Real AWS
// faults DBInstanceNotFound when describing a deleted instance by identifier —
// this is what lets the DBInstanceDeleted waiter complete.
t.Run("DeleteDBInstance", func(t *testing.T) {
_, err := svc.DeleteDBInstance(ctx, &rds.DeleteDBInstanceInput{
DBInstanceIdentifier: aws.String(instanceId),
SkipFinalSnapshot: aws.Bool(true),
})
require.NoError(t, err)
_, err = svc.DescribeDBInstances(ctx, &rds.DescribeDBInstancesInput{
DBInstanceIdentifier: aws.String(instanceId),
})
var notFound *types.DBInstanceNotFoundFault
require.ErrorAs(t, err, &notFound,
"describe of a deleted instance must fault DBInstanceNotFound")
})
}
// TestRDSCluster covers management-plane CRUD for a DB cluster and validates
// that DBSubnetGroup is returned as a plain string field (fix #548).
func TestRDSCluster(t *testing.T) {
ctx := context.Background()
svc := testutil.RDSClient()
clusterId := fmt.Sprintf("go-cluster-%d", time.Now().UnixMilli()%100000)
t.Cleanup(func() {
svc.DeleteDBCluster(ctx, &rds.DeleteDBClusterInput{ //nolint:errcheck
DBClusterIdentifier: aws.String(clusterId),
SkipFinalSnapshot: aws.Bool(true),
})
})
t.Run("CreateDBCluster", func(t *testing.T) {
out, err := svc.CreateDBCluster(ctx, &rds.CreateDBClusterInput{
DBClusterIdentifier: aws.String(clusterId),
Engine: aws.String("aurora-postgresql"),
MasterUsername: aws.String(rdsUsername),
MasterUserPassword: aws.String(rdsPassword),
DatabaseName: aws.String(rdsDatabase),
})
require.NoError(t, err)
require.NotNil(t, out.DBCluster)
assert.Equal(t, clusterId, aws.ToString(out.DBCluster.DBClusterIdentifier))
assert.Equal(t, rdsUsername, aws.ToString(out.DBCluster.MasterUsername))
})
// Fix #548: DBCluster.DBSubnetGroup must unmarshal as a plain *string, not a nested
// struct. The AWS service model defines it as shape: String. If the XML had nested
// elements (DBSubnetGroupName, etc.), the Go SDK would return nil for this field.
t.Run("DescribeDBCluster_DBSubnetGroupIsString", func(t *testing.T) {
out, err := svc.DescribeDBClusters(ctx, &rds.DescribeDBClustersInput{
DBClusterIdentifier: aws.String(clusterId),
})
require.NoError(t, err)
require.Len(t, out.DBClusters, 1)
cluster := out.DBClusters[0]
assert.Equal(t, clusterId, aws.ToString(cluster.DBClusterIdentifier))
assert.Equal(t, rdsUsername, aws.ToString(cluster.MasterUsername))
assert.NotNil(t, cluster.DBSubnetGroup,
"DBCluster.DBSubnetGroup must be a non-nil string (shape: String in AWS service model)")
})
// Fix #567: deleting a cluster must remove it from the describe list. Real AWS
// faults DBClusterNotFoundFault when describing a deleted cluster by identifier —
// this is what lets the DBClusterDeleted waiter complete.
t.Run("DeleteDBCluster", func(t *testing.T) {
_, err := svc.DeleteDBCluster(ctx, &rds.DeleteDBClusterInput{
DBClusterIdentifier: aws.String(clusterId),
SkipFinalSnapshot: aws.Bool(true),
})
require.NoError(t, err)
_, err = svc.DescribeDBClusters(ctx, &rds.DescribeDBClustersInput{
DBClusterIdentifier: aws.String(clusterId),
})
var notFound *types.DBClusterNotFoundFault
require.ErrorAs(t, err, &notFound,
"describe of a deleted cluster must fault DBClusterNotFoundFault")
})
}
// ── Helpers ──────────────────────────────────────────────────────────────────
func openPostgresConn(host string, port int, user, password, dbname string) (*sql.DB, error) {
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable connect_timeout=5",
host, port, user, password, dbname)
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
db.Close()
return nil, err
}
return db, nil
}
func awaitPostgresConn(t *testing.T, host string, port int, user, password, dbname string) *sql.DB {
t.Helper()
deadline := time.Now().Add(60 * time.Second)
var lastErr error
for time.Now().Before(deadline) {
db, err := openPostgresConn(host, port, user, password, dbname)
if err == nil {
return db
}
lastErr = err
time.Sleep(time.Second)
}
t.Fatalf("timed out waiting for postgres connection on %s:%d: %v", host, port, lastErr)
return nil
}
@@ -0,0 +1,245 @@
package tests
import (
"context"
"fmt"
"net/http"
"strings"
"testing"
"time"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestS3CORS(t *testing.T) {
ctx := context.Background()
svc := testutil.S3Client()
baseURL := testutil.Endpoint()
bucket := fmt.Sprintf("go-cors-test-%d", time.Now().UnixMilli())
objectKey := "cors-test.txt"
// raw sends a raw HTTP request and returns the status code and response headers.
raw := func(method, path string, headers map[string]string) (int, http.Header, error) {
rawURL := baseURL + "/" + bucket + path
req, err := http.NewRequest(method, rawURL, nil)
if err != nil {
return 0, nil, err
}
for k, v := range headers {
req.Header.Set(k, v)
}
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Do(req)
if err != nil {
return 0, nil, err
}
defer resp.Body.Close()
return resp.StatusCode, resp.Header, nil
}
varyHasOrigin := func(vary string) bool {
for _, tok := range strings.Split(vary, ",") {
if strings.EqualFold(strings.TrimSpace(tok), "origin") {
return true
}
}
return false
}
t.Cleanup(func() {
svc.DeleteBucketCors(ctx, &s3.DeleteBucketCorsInput{Bucket: aws.String(bucket)})
svc.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: aws.String(objectKey)})
svc.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: aws.String(bucket)})
})
// Setup
t.Run("Setup", func(t *testing.T) {
_, err := svc.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: aws.String(bucket)})
require.NoError(t, err)
_, err = svc.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(objectKey),
Body: strings.NewReader("hello cors"),
ContentType: aws.String("text/plain"),
})
require.NoError(t, err)
})
t.Run("PreflightWithoutConfig_Returns403", func(t *testing.T) {
status, _, err := raw("OPTIONS", "/"+objectKey, map[string]string{
"Origin": "http://localhost:3000",
"Access-Control-Request-Method": "GET",
})
require.NoError(t, err)
assert.Equal(t, 403, status)
})
t.Run("WildcardCORS", func(t *testing.T) {
maxAge := int32(3000)
_, err := svc.PutBucketCors(ctx, &s3.PutBucketCorsInput{
Bucket: aws.String(bucket),
CORSConfiguration: &s3types.CORSConfiguration{
CORSRules: []s3types.CORSRule{
{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "PUT", "POST", "DELETE", "HEAD"},
AllowedHeaders: []string{"*"},
ExposeHeaders: []string{"ETag"},
MaxAgeSeconds: &maxAge,
},
},
},
})
require.NoError(t, err)
// Preflight
status, hdrs, err := raw("OPTIONS", "/"+objectKey, map[string]string{
"Origin": "http://localhost:3000",
"Access-Control-Request-Method": "GET",
})
require.NoError(t, err)
assert.Equal(t, 200, status)
assert.Equal(t, "*", hdrs.Get("Access-Control-Allow-Origin"))
assert.Equal(t, "3000", hdrs.Get("Access-Control-Max-Age"))
assert.Contains(t, strings.ToUpper(hdrs.Get("Access-Control-Allow-Methods")), "GET")
// Actual GET with Origin
status, hdrs, err = raw("GET", "/"+objectKey, map[string]string{"Origin": "http://localhost:3000"})
require.NoError(t, err)
assert.Equal(t, "*", hdrs.Get("Access-Control-Allow-Origin"))
assert.True(t, varyHasOrigin(hdrs.Get("Vary")))
assert.Contains(t, hdrs.Get("Access-Control-Expose-Headers"), "ETag")
// GET without Origin - no CORS headers
_, hdrs, err = raw("GET", "/"+objectKey, nil)
require.NoError(t, err)
assert.Empty(t, hdrs.Get("Access-Control-Allow-Origin"))
// OPTIONS without Origin - no CORS headers
_, hdrs, err = raw("OPTIONS", "/"+objectKey, nil)
require.NoError(t, err)
assert.Empty(t, hdrs.Get("Access-Control-Allow-Origin"))
})
t.Run("SpecificOriginCORS", func(t *testing.T) {
maxAge := int32(600)
_, err := svc.PutBucketCors(ctx, &s3.PutBucketCorsInput{
Bucket: aws.String(bucket),
CORSConfiguration: &s3types.CORSConfiguration{
CORSRules: []s3types.CORSRule{
{
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET", "PUT"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
ExposeHeaders: []string{"ETag", "x-amz-request-id"},
MaxAgeSeconds: &maxAge,
},
},
},
})
require.NoError(t, err)
// Matching origin
status, hdrs, err := raw("OPTIONS", "/"+objectKey, map[string]string{
"Origin": "https://example.com",
"Access-Control-Request-Method": "GET",
"Access-Control-Request-Headers": "Content-Type",
})
require.NoError(t, err)
assert.Equal(t, 200, status)
assert.Equal(t, "https://example.com", hdrs.Get("Access-Control-Allow-Origin"))
assert.Equal(t, "600", hdrs.Get("Access-Control-Max-Age"))
// Non-matching origin
status, _, err = raw("OPTIONS", "/"+objectKey, map[string]string{
"Origin": "https://attacker.evil.com",
"Access-Control-Request-Method": "GET",
})
require.NoError(t, err)
assert.Equal(t, 403, status)
// Non-matching method
status, _, err = raw("OPTIONS", "/"+objectKey, map[string]string{
"Origin": "https://example.com",
"Access-Control-Request-Method": "DELETE",
})
require.NoError(t, err)
assert.Equal(t, 403, status)
// Actual GET matching
_, hdrs, err = raw("GET", "/"+objectKey, map[string]string{"Origin": "https://example.com"})
require.NoError(t, err)
assert.Equal(t, "https://example.com", hdrs.Get("Access-Control-Allow-Origin"))
// Actual GET non-matching
_, hdrs, err = raw("GET", "/"+objectKey, map[string]string{"Origin": "https://not-allowed.com"})
require.NoError(t, err)
assert.Empty(t, hdrs.Get("Access-Control-Allow-Origin"))
})
t.Run("DeleteBucketCors_PreflightReturns403", func(t *testing.T) {
_, err := svc.DeleteBucketCors(ctx, &s3.DeleteBucketCorsInput{Bucket: aws.String(bucket)})
require.NoError(t, err)
status, _, err := raw("OPTIONS", "/"+objectKey, map[string]string{
"Origin": "http://localhost:3000",
"Access-Control-Request-Method": "GET",
})
require.NoError(t, err)
assert.Equal(t, 403, status)
})
t.Run("SubdomainWildcardCORS", func(t *testing.T) {
maxAge := int32(120)
_, err := svc.PutBucketCors(ctx, &s3.PutBucketCorsInput{
Bucket: aws.String(bucket),
CORSConfiguration: &s3types.CORSConfiguration{
CORSRules: []s3types.CORSRule{
{
AllowedOrigins: []string{"http://*.example.com"},
AllowedMethods: []string{"GET"},
AllowedHeaders: []string{"*"},
MaxAgeSeconds: &maxAge,
},
},
},
})
require.NoError(t, err)
// Matching subdomain
status, hdrs, err := raw("OPTIONS", "/"+objectKey, map[string]string{
"Origin": "http://app.example.com",
"Access-Control-Request-Method": "GET",
})
require.NoError(t, err)
assert.Equal(t, 200, status)
assert.Equal(t, "http://app.example.com", hdrs.Get("Access-Control-Allow-Origin"))
// Wrong scheme (https instead of http)
status, _, err = raw("OPTIONS", "/"+objectKey, map[string]string{
"Origin": "https://app.example.com",
"Access-Control-Request-Method": "GET",
})
require.NoError(t, err)
assert.Equal(t, 403, status)
// Different domain
status, _, err = raw("OPTIONS", "/"+objectKey, map[string]string{
"Origin": "http://app.other.com",
"Access-Control-Request-Method": "GET",
})
require.NoError(t, err)
assert.Equal(t, 403, status)
})
}
@@ -0,0 +1,226 @@
package tests
import (
"bytes"
"context"
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"hash/crc32"
"io"
"math/rand"
"strings"
"testing"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Multipart uploads through the S3 transfer manager, the code path terraform-provider-aws uses for
// aws_s3_object. S3 stores a COMPOSITE checksum for SHA algorithms (the algorithm applied to the
// concatenated part checksums, suffixed with "-<parts>") and reports it on HeadObject, without the
// suffix on GetObjectAttributes. A copy is written as a single object with a FULL_OBJECT checksum.
const checksumPartSize = 5 * 1024 * 1024
func checksumPayload(size int) []byte {
b := make([]byte, size)
rand.New(rand.NewSource(20260903)).Read(b)
return b
}
func splitParts(data []byte, size int) [][]byte {
var parts [][]byte
for start := 0; start < len(data); start += size {
end := start + size
if end > len(data) {
end = len(data)
}
parts = append(parts, data[start:end])
}
return parts
}
func sha256B64(data []byte) string {
d := sha256.Sum256(data)
return base64.StdEncoding.EncodeToString(d[:])
}
func sha256Composite(parts [][]byte) string {
var joined []byte
for _, p := range parts {
d := sha256.Sum256(p)
joined = append(joined, d[:]...)
}
return sha256B64(joined) + fmt.Sprintf("-%d", len(parts))
}
func crc32Bytes(data []byte) []byte {
v := crc32.ChecksumIEEE(data)
return []byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}
}
func crc32Composite(parts [][]byte) string {
var joined []byte
for _, p := range parts {
joined = append(joined, crc32Bytes(p)...)
}
return base64.StdEncoding.EncodeToString(crc32Bytes(joined)) + fmt.Sprintf("-%d", len(parts))
}
// crc64Nvme implements CRC-64/NVME (reflected polynomial 0x9a6c9329ac4bc9b5), the checksum S3
// attaches when a client declares no algorithm.
func crc64Nvme(data []byte) []byte {
var table [256]uint64
for i := range table {
c := uint64(i)
for j := 0; j < 8; j++ {
if c&1 == 1 {
c = (c >> 1) ^ 0x9a6c9329ac4bc9b5
} else {
c >>= 1
}
}
table[i] = c
}
c := ^uint64(0)
for _, b := range data {
c = table[byte(c)^b] ^ (c >> 8)
}
c = ^c
out := make([]byte, 8)
for i := 0; i < 8; i++ {
out[i] = byte(c >> (56 - 8*i))
}
return out
}
func TestS3MultipartChecksums(t *testing.T) {
ctx := context.Background()
svc := testutil.S3Client()
bucket := "go-test-multipart-checksums"
payload := checksumPayload(12 * 1024 * 1024)
parts := splitParts(payload, checksumPartSize)
require.Len(t, parts, 3)
uploader := manager.NewUploader(svc, func(u *manager.Uploader) {
u.PartSize = checksumPartSize
})
keys := []string{"sha256.bin", "default.bin", "sha256-copy.bin"}
_, err := svc.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: aws.String(bucket)})
require.NoError(t, err)
t.Cleanup(func() {
for _, key := range keys {
if _, err := svc.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: aws.String(key)}); err != nil {
t.Logf("cleanup: could not delete s3://%s/%s: %v", bucket, key, err)
}
}
if _, err := svc.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: aws.String(bucket)}); err != nil {
t.Logf("cleanup: could not delete bucket %s: %v", bucket, err)
}
})
head := func(t *testing.T, key string) *s3.HeadObjectOutput {
out, err := svc.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(bucket), Key: aws.String(key), ChecksumMode: s3types.ChecksumModeEnabled,
})
require.NoError(t, err)
return out
}
t.Run("SHA256 upload reports the composite checksum", func(t *testing.T) {
key := keys[0]
_, err := uploader.Upload(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket), Key: aws.String(key), Body: bytes.NewReader(payload),
ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256,
})
require.NoError(t, err)
composite := sha256Composite(parts)
h := head(t, key)
assert.Equal(t, composite, aws.ToString(h.ChecksumSHA256))
assert.Equal(t, s3types.ChecksumTypeComposite, h.ChecksumType)
assert.True(t, strings.HasSuffix(aws.ToString(h.ETag), `-3"`), aws.ToString(h.ETag))
attrs, err := svc.GetObjectAttributes(ctx, &s3.GetObjectAttributesInput{
Bucket: aws.String(bucket), Key: aws.String(key),
ObjectAttributes: []s3types.ObjectAttributes{
s3types.ObjectAttributesChecksum, s3types.ObjectAttributesObjectParts, s3types.ObjectAttributesEtag,
},
})
require.NoError(t, err)
assert.Equal(t, strings.TrimSuffix(composite, "-3"), aws.ToString(attrs.Checksum.ChecksumSHA256),
"GetObjectAttributes omits the part-count suffix")
assert.Equal(t, s3types.ChecksumTypeComposite, attrs.Checksum.ChecksumType)
assert.Equal(t, int32(3), aws.ToInt32(attrs.ObjectParts.TotalPartsCount))
for i, part := range attrs.ObjectParts.Parts {
assert.Equal(t, sha256B64(parts[i]), aws.ToString(part.ChecksumSHA256), "part %d", i+1)
}
assert.Equal(t, strings.Trim(aws.ToString(h.ETag), `"`), aws.ToString(attrs.ETag),
"GetObjectAttributes returns the ETag without quotes")
})
t.Run("default checksum of the uploader matches what S3 stores", func(t *testing.T) {
key := keys[1]
_, err := uploader.Upload(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket), Key: aws.String(key), Body: bytes.NewReader(payload),
})
require.NoError(t, err)
// Which algorithm and type the SDK picks depends on its version; whatever it sent, the stored
// value must be the one S3 would compute for that combination.
h := head(t, key)
switch {
case h.ChecksumCRC32 != nil && h.ChecksumType == s3types.ChecksumTypeComposite:
assert.Equal(t, crc32Composite(parts), aws.ToString(h.ChecksumCRC32))
case h.ChecksumCRC32 != nil && h.ChecksumType == s3types.ChecksumTypeFullObject:
assert.Equal(t, base64.StdEncoding.EncodeToString(crc32Bytes(payload)), aws.ToString(h.ChecksumCRC32))
case h.ChecksumCRC64NVME != nil:
assert.Equal(t, s3types.ChecksumTypeFullObject, h.ChecksumType)
assert.Equal(t, base64.StdEncoding.EncodeToString(crc64Nvme(payload)), aws.ToString(h.ChecksumCRC64NVME))
default:
t.Fatalf("unexpected checksum on HeadObject: %+v", h)
}
t.Logf("uploader default: CRC32=%q CRC64NVME=%q type=%s",
aws.ToString(h.ChecksumCRC32), aws.ToString(h.ChecksumCRC64NVME), h.ChecksumType)
// Downloading with response checksum validation enabled must round-trip the payload.
obj, err := svc.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucket), Key: aws.String(key), ChecksumMode: s3types.ChecksumModeEnabled,
})
require.NoError(t, err)
body, err := io.ReadAll(obj.Body)
require.NoError(t, err)
obj.Body.Close()
assert.True(t, bytes.Equal(payload, body), "downloaded body differs from the payload")
})
t.Run("copy of the multipart object is a single full object", func(t *testing.T) {
source := head(t, keys[0])
out, err := svc.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(bucket), Key: aws.String(keys[2]),
CopySource: aws.String(bucket + "/" + keys[0]),
})
require.NoError(t, err)
sum := md5.Sum(payload)
expectedETag := `"` + hex.EncodeToString(sum[:]) + `"`
assert.Equal(t, sha256B64(payload), aws.ToString(out.CopyObjectResult.ChecksumSHA256))
assert.Equal(t, s3types.ChecksumTypeFullObject, out.CopyObjectResult.ChecksumType)
assert.Equal(t, expectedETag, aws.ToString(out.CopyObjectResult.ETag))
assert.NotEqual(t, aws.ToString(source.ETag), aws.ToString(out.CopyObjectResult.ETag))
h := head(t, keys[2])
assert.Equal(t, sha256B64(payload), aws.ToString(h.ChecksumSHA256))
assert.Equal(t, s3types.ChecksumTypeFullObject, h.ChecksumType)
assert.Equal(t, expectedETag, aws.ToString(h.ETag))
})
}
@@ -0,0 +1,116 @@
package tests
import (
"context"
"testing"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/aws-sdk-go-v2/service/sns"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestS3Notifications(t *testing.T) {
ctx := context.Background()
s3Svc := testutil.S3Client()
sqsSvc := testutil.SQSClient()
snsSvc := testutil.SNSClient()
queueName := "s3-notif-filter-queue"
topicName := "s3-notif-filter-topic"
bucketName := "s3-notif-filter-bucket"
queueArn := "arn:aws:sqs:us-east-1:000000000000:" + queueName
var topicArn string
var queueURL string
t.Cleanup(func() {
s3Svc.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: aws.String(bucketName)})
if queueURL != "" {
sqsSvc.DeleteQueue(ctx, &sqs.DeleteQueueInput{QueueUrl: aws.String(queueURL)})
}
if topicArn != "" {
snsSvc.DeleteTopic(ctx, &sns.DeleteTopicInput{TopicArn: aws.String(topicArn)})
}
})
t.Run("Setup", func(t *testing.T) {
// Create SQS queue
qr, err := sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(queueName)})
require.NoError(t, err)
queueURL = aws.ToString(qr.QueueUrl)
// Create SNS topic
tr, err := snsSvc.CreateTopic(ctx, &sns.CreateTopicInput{Name: aws.String(topicName)})
require.NoError(t, err)
topicArn = aws.ToString(tr.TopicArn)
// Create S3 bucket
_, err = s3Svc.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: aws.String(bucketName)})
require.NoError(t, err)
})
t.Run("PutBucketNotificationConfiguration", func(t *testing.T) {
_, err := s3Svc.PutBucketNotificationConfiguration(ctx, &s3.PutBucketNotificationConfigurationInput{
Bucket: aws.String(bucketName),
NotificationConfiguration: &s3types.NotificationConfiguration{
QueueConfigurations: []s3types.QueueConfiguration{
{
Id: aws.String("sqs-filtered"),
QueueArn: aws.String(queueArn),
Events: []s3types.Event{s3types.EventS3ObjectCreated},
Filter: &s3types.NotificationConfigurationFilter{
Key: &s3types.S3KeyFilter{
FilterRules: []s3types.FilterRule{
{Name: s3types.FilterRuleNamePrefix, Value: aws.String("incoming/")},
{Name: s3types.FilterRuleNameSuffix, Value: aws.String(".csv")},
},
},
},
},
},
TopicConfigurations: []s3types.TopicConfiguration{
{
Id: aws.String("sns-filtered"),
TopicArn: aws.String(topicArn),
Events: []s3types.Event{s3types.EventS3ObjectRemoved},
Filter: &s3types.NotificationConfigurationFilter{
Key: &s3types.S3KeyFilter{
FilterRules: []s3types.FilterRule{
{Name: s3types.FilterRuleNamePrefix, Value: aws.String("")},
{Name: s3types.FilterRuleNameSuffix, Value: aws.String(".txt")},
},
},
},
},
},
},
})
require.NoError(t, err)
})
t.Run("GetBucketNotificationConfiguration", func(t *testing.T) {
r, err := s3Svc.GetBucketNotificationConfiguration(ctx, &s3.GetBucketNotificationConfigurationInput{
Bucket: aws.String(bucketName),
})
require.NoError(t, err)
// Assert queue config
require.NotEmpty(t, r.QueueConfigurations)
assert.Equal(t, queueArn, aws.ToString(r.QueueConfigurations[0].QueueArn))
require.NotNil(t, r.QueueConfigurations[0].Filter)
require.NotNil(t, r.QueueConfigurations[0].Filter.Key)
assert.Len(t, r.QueueConfigurations[0].Filter.Key.FilterRules, 2)
// Assert topic config
require.NotEmpty(t, r.TopicConfigurations)
assert.Equal(t, topicArn, aws.ToString(r.TopicConfigurations[0].TopicArn))
require.NotNil(t, r.TopicConfigurations[0].Filter)
require.NotNil(t, r.TopicConfigurations[0].Filter.Key)
assert.Len(t, r.TopicConfigurations[0].Filter.Key.FilterRules, 2)
})
}
@@ -0,0 +1,299 @@
package tests
import (
"bytes"
"context"
"net/url"
"strings"
"testing"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestS3(t *testing.T) {
ctx := context.Background()
svc := testutil.S3Client()
bucket := "go-test-bucket"
key := "test-object.json"
content := `{"source":"go-test"}`
// Cleanup at end
t.Cleanup(func() {
svc.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: aws.String(key)})
svc.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: aws.String("copy-" + key)})
svc.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: aws.String(bucket)})
})
t.Run("CreateBucket", func(t *testing.T) {
_, err := svc.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: aws.String(bucket)})
require.NoError(t, err)
})
t.Run("ListBuckets", func(t *testing.T) {
r, err := svc.ListBuckets(ctx, &s3.ListBucketsInput{})
require.NoError(t, err)
assert.NotEmpty(t, r.Buckets)
})
t.Run("PutObject", func(t *testing.T) {
_, err := svc.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: strings.NewReader(content),
ContentType: aws.String("application/json"),
})
require.NoError(t, err)
})
t.Run("GetObject", func(t *testing.T) {
out, err := svc.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
require.NoError(t, err)
defer out.Body.Close()
var buf bytes.Buffer
buf.ReadFrom(out.Body)
assert.Equal(t, content, buf.String())
})
t.Run("HeadObject", func(t *testing.T) {
head, err := svc.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
require.NoError(t, err)
assert.NotNil(t, head.LastModified)
assert.Zero(t, head.LastModified.Nanosecond(), "LastModified should have second precision")
})
t.Run("ListObjectsV2", func(t *testing.T) {
r, err := svc.ListObjectsV2(ctx, &s3.ListObjectsV2Input{Bucket: aws.String(bucket)})
require.NoError(t, err)
assert.NotEmpty(t, r.Contents)
})
t.Run("CopyObject", func(t *testing.T) {
_, err := svc.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(bucket),
CopySource: aws.String(bucket + "/" + key),
Key: aws.String("copy-" + key),
})
require.NoError(t, err)
})
t.Run("DeleteObject", func(t *testing.T) {
_, err := svc.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
require.NoError(t, err)
})
t.Run("DeleteBucket", func(t *testing.T) {
// First delete the copy
svc.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(bucket),
Key: aws.String("copy-" + key),
})
_, err := svc.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: aws.String(bucket)})
require.NoError(t, err)
})
}
func TestS3LocationConstraint(t *testing.T) {
ctx := context.Background()
svc := testutil.S3Client()
euBucket := "go-test-bucket-eu"
t.Cleanup(func() {
svc.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: aws.String(euBucket)})
})
t.Run("CreateBucketWithLocationConstraint", func(t *testing.T) {
_, err := svc.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: aws.String(euBucket),
CreateBucketConfiguration: &s3types.CreateBucketConfiguration{
LocationConstraint: s3types.BucketLocationConstraintEuCentral1,
},
})
require.NoError(t, err)
})
t.Run("GetBucketLocation", func(t *testing.T) {
loc, err := svc.GetBucketLocation(ctx, &s3.GetBucketLocationInput{Bucket: aws.String(euBucket)})
require.NoError(t, err)
assert.Equal(t, "eu-central-1", string(loc.LocationConstraint))
})
}
// TestS3NonASCIIKey tests CopyObject with non-ASCII (multibyte) keys.
// Regression test for issue #93.
func TestS3NonASCIIKey(t *testing.T) {
ctx := context.Background()
svc := testutil.S3Client()
bucket := "go-test-non-ascii"
srcKey := "src/テスト画像.png"
dstKey := "dst/テスト画像.png"
t.Cleanup(func() {
svc.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: aws.String(srcKey)})
svc.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: aws.String(dstKey)})
svc.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: aws.String(bucket)})
})
// Setup
_, err := svc.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: aws.String(bucket)})
require.NoError(t, err)
_, err = svc.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(srcKey),
Body: strings.NewReader("non-ascii content"),
})
require.NoError(t, err)
// Copy with non-ASCII key
_, err = svc.CopyObject(ctx, &s3.CopyObjectInput{
Bucket: aws.String(bucket),
CopySource: aws.String(bucket + "/" + url.PathEscape(srcKey)),
Key: aws.String(dstKey),
})
require.NoError(t, err, "CopyObject with non-ASCII key should succeed")
// Verify the copy exists
_, err = svc.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(dstKey),
})
require.NoError(t, err, "copied object should exist")
}
// TestS3MultipartCopyNonASCIIKey exercises UploadPartCopy with a URL-encoded
// non-ASCII source key to cover the multipart copy code path.
func TestS3MultipartCopyNonASCIIKey(t *testing.T) {
ctx := context.Background()
svc := testutil.S3Client()
bucket := "go-test-multipart-copy-non-ascii"
srcKey := "src/テスト画像.bin"
dstKey := "dst/テスト画像.bin"
content := bytes.Repeat([]byte("a"), 6*1024*1024)
var uploadID string
t.Cleanup(func() {
if uploadID != "" {
svc.AbortMultipartUpload(ctx, &s3.AbortMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(dstKey),
UploadId: aws.String(uploadID),
})
}
svc.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: aws.String(srcKey)})
svc.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: aws.String(dstKey)})
svc.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: aws.String(bucket)})
})
_, err := svc.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: aws.String(bucket)})
require.NoError(t, err)
_, err = svc.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(srcKey),
Body: bytes.NewReader(content),
})
require.NoError(t, err)
createOut, err := svc.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(dstKey),
})
require.NoError(t, err)
uploadID = aws.ToString(createOut.UploadId)
require.NotEmpty(t, uploadID)
partOut, err := svc.UploadPartCopy(ctx, &s3.UploadPartCopyInput{
Bucket: aws.String(bucket),
Key: aws.String(dstKey),
UploadId: aws.String(uploadID),
PartNumber: aws.Int32(1),
CopySource: aws.String(bucket + "/" + url.PathEscape(srcKey)),
})
require.NoError(t, err, "UploadPartCopy with non-ASCII key should succeed")
require.NotNil(t, partOut.CopyPartResult)
require.NotNil(t, partOut.CopyPartResult.ETag)
_, err = svc.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(dstKey),
UploadId: aws.String(uploadID),
MultipartUpload: &s3types.CompletedMultipartUpload{
Parts: []s3types.CompletedPart{
{
ETag: partOut.CopyPartResult.ETag,
PartNumber: aws.Int32(1),
},
},
},
})
require.NoError(t, err)
uploadID = ""
getOut, err := svc.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(dstKey),
})
require.NoError(t, err)
defer getOut.Body.Close()
var copied bytes.Buffer
_, err = copied.ReadFrom(getOut.Body)
require.NoError(t, err)
assert.Equal(t, content, copied.Bytes())
}
// TestS3LargeObject tests uploading a 25 MB object.
// Validates upload size limit handling.
func TestS3LargeObject(t *testing.T) {
ctx := context.Background()
svc := testutil.S3Client()
bucket := "go-test-large-upload"
key := "large-object-25mb.bin"
size := int64(25 * 1024 * 1024) // 25 MB
t.Cleanup(func() {
svc.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: aws.String(key)})
svc.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: aws.String(bucket)})
})
// Setup
_, err := svc.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: aws.String(bucket)})
require.NoError(t, err)
// Create 25 MB payload
payload := make([]byte, size)
_, err = svc.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: bytes.NewReader(payload),
ContentType: aws.String("application/octet-stream"),
ContentLength: aws.Int64(size),
})
require.NoError(t, err, "PutObject 25 MB should succeed")
// Verify content-length via HeadObject
head, err := svc.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
require.NoError(t, err)
assert.Equal(t, size, aws.ToInt64(head.ContentLength), "content-length should be 25 MB")
}
@@ -0,0 +1,79 @@
package tests
import (
"context"
"testing"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSecretsManager(t *testing.T) {
ctx := context.Background()
svc := testutil.SecretsManagerClient()
secretName := "go-test/creds"
secretVal := `{"username":"admin","password":"s3cret"}`
var secretARN string
t.Cleanup(func() {
if secretARN != "" {
svc.DeleteSecret(ctx, &secretsmanager.DeleteSecretInput{
SecretId: aws.String(secretARN),
ForceDeleteWithoutRecovery: aws.Bool(true),
})
}
})
t.Run("CreateSecret", func(t *testing.T) {
r, err := svc.CreateSecret(ctx, &secretsmanager.CreateSecretInput{
Name: aws.String(secretName),
SecretString: aws.String(secretVal),
})
require.NoError(t, err)
secretARN = aws.ToString(r.ARN)
assert.NotEmpty(t, secretARN)
})
t.Run("GetSecretValue", func(t *testing.T) {
r, err := svc.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{
SecretId: aws.String(secretARN),
})
require.NoError(t, err)
assert.Equal(t, secretVal, aws.ToString(r.SecretString))
})
t.Run("DescribeSecret", func(t *testing.T) {
_, err := svc.DescribeSecret(ctx, &secretsmanager.DescribeSecretInput{
SecretId: aws.String(secretARN),
})
require.NoError(t, err)
})
t.Run("ListSecrets", func(t *testing.T) {
r, err := svc.ListSecrets(ctx, &secretsmanager.ListSecretsInput{})
require.NoError(t, err)
assert.NotEmpty(t, r.SecretList)
})
t.Run("PutSecretValue", func(t *testing.T) {
newVal := `{"username":"admin","password":"n3wsecret"}`
_, err := svc.PutSecretValue(ctx, &secretsmanager.PutSecretValueInput{
SecretId: aws.String(secretARN),
SecretString: aws.String(newVal),
})
require.NoError(t, err)
})
t.Run("DeleteSecret", func(t *testing.T) {
_, err := svc.DeleteSecret(ctx, &secretsmanager.DeleteSecretInput{
SecretId: aws.String(secretARN),
ForceDeleteWithoutRecovery: aws.Bool(true),
})
require.NoError(t, err)
secretARN = "" // Prevent double cleanup
})
}
@@ -0,0 +1,122 @@
package tests
import (
"context"
"testing"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sns"
"github.com/aws/aws-sdk-go-v2/service/sqs"
sqstypes "github.com/aws/aws-sdk-go-v2/service/sqs/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSNS(t *testing.T) {
ctx := context.Background()
svc := testutil.SNSClient()
sqsSvc := testutil.SQSClient()
topicName := "go-test-topic"
queueName := "go-sns-target"
var topicARN string
var queueURL string
var subARN string
t.Cleanup(func() {
if subARN != "" {
svc.Unsubscribe(ctx, &sns.UnsubscribeInput{SubscriptionArn: aws.String(subARN)})
}
if topicARN != "" {
svc.DeleteTopic(ctx, &sns.DeleteTopicInput{TopicArn: aws.String(topicARN)})
}
if queueURL != "" {
sqsSvc.DeleteQueue(ctx, &sqs.DeleteQueueInput{QueueUrl: aws.String(queueURL)})
}
})
t.Run("CreateTopic", func(t *testing.T) {
r, err := svc.CreateTopic(ctx, &sns.CreateTopicInput{Name: aws.String(topicName)})
require.NoError(t, err)
topicARN = aws.ToString(r.TopicArn)
assert.NotEmpty(t, topicARN)
})
t.Run("ListTopics", func(t *testing.T) {
r, err := svc.ListTopics(ctx, &sns.ListTopicsInput{})
require.NoError(t, err)
assert.NotEmpty(t, r.Topics)
})
t.Run("GetTopicAttributes", func(t *testing.T) {
r, err := svc.GetTopicAttributes(ctx, &sns.GetTopicAttributesInput{TopicArn: aws.String(topicARN)})
require.NoError(t, err)
assert.Equal(t, topicARN, r.Attributes["TopicArn"])
})
t.Run("Subscribe", func(t *testing.T) {
// Create target SQS queue
qr, err := sqsSvc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(queueName)})
require.NoError(t, err)
queueURL = aws.ToString(qr.QueueUrl)
// Get queue ARN
attr, err := sqsSvc.GetQueueAttributes(ctx, &sqs.GetQueueAttributesInput{
QueueUrl: aws.String(queueURL),
AttributeNames: []sqstypes.QueueAttributeName{sqstypes.QueueAttributeNameAll},
})
require.NoError(t, err)
queueARN := attr.Attributes["QueueArn"]
r, err := svc.Subscribe(ctx, &sns.SubscribeInput{
TopicArn: aws.String(topicARN),
Protocol: aws.String("sqs"),
Endpoint: aws.String(queueARN),
})
require.NoError(t, err)
subARN = aws.ToString(r.SubscriptionArn)
assert.NotEmpty(t, subARN)
})
t.Run("ListSubscriptions", func(t *testing.T) {
r, err := svc.ListSubscriptions(ctx, &sns.ListSubscriptionsInput{})
require.NoError(t, err)
assert.NotEmpty(t, r.Subscriptions)
})
t.Run("Publish", func(t *testing.T) {
r, err := svc.Publish(ctx, &sns.PublishInput{
TopicArn: aws.String(topicARN),
Message: aws.String(`{"event":"go-test"}`),
Subject: aws.String("GoTest"),
})
require.NoError(t, err)
assert.NotEmpty(t, aws.ToString(r.MessageId))
})
t.Run("GetSubscriptionAttributes", func(t *testing.T) {
if subARN == "" {
t.Skip("No subscription created")
}
_, err := svc.GetSubscriptionAttributes(ctx, &sns.GetSubscriptionAttributesInput{
SubscriptionArn: aws.String(subARN),
})
require.NoError(t, err)
})
t.Run("Unsubscribe", func(t *testing.T) {
if subARN == "" {
t.Skip("No subscription created")
}
_, err := svc.Unsubscribe(ctx, &sns.UnsubscribeInput{SubscriptionArn: aws.String(subARN)})
require.NoError(t, err)
subARN = "" // Prevent double cleanup
})
t.Run("DeleteTopic", func(t *testing.T) {
_, err := svc.DeleteTopic(ctx, &sns.DeleteTopicInput{TopicArn: aws.String(topicARN)})
require.NoError(t, err)
topicARN = "" // Prevent double cleanup
})
}
@@ -0,0 +1,119 @@
package tests
import (
"context"
"testing"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sqs"
sqstypes "github.com/aws/aws-sdk-go-v2/service/sqs/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSQS(t *testing.T) {
ctx := context.Background()
svc := testutil.SQSClient()
qName := "go-test-queue"
var qURL string
// Cleanup at end
t.Cleanup(func() {
if qURL != "" {
svc.DeleteQueue(ctx, &sqs.DeleteQueueInput{QueueUrl: aws.String(qURL)})
}
})
t.Run("CreateQueue", func(t *testing.T) {
r, err := svc.CreateQueue(ctx, &sqs.CreateQueueInput{QueueName: aws.String(qName)})
require.NoError(t, err)
qURL = aws.ToString(r.QueueUrl)
assert.NotEmpty(t, qURL)
})
t.Run("ListQueues", func(t *testing.T) {
r, err := svc.ListQueues(ctx, &sqs.ListQueuesInput{})
require.NoError(t, err)
assert.NotEmpty(t, r.QueueUrls)
})
t.Run("GetQueueUrl", func(t *testing.T) {
r, err := svc.GetQueueUrl(ctx, &sqs.GetQueueUrlInput{QueueName: aws.String(qName)})
require.NoError(t, err)
assert.Equal(t, qURL, aws.ToString(r.QueueUrl))
})
t.Run("GetQueueAttributes", func(t *testing.T) {
r, err := svc.GetQueueAttributes(ctx, &sqs.GetQueueAttributesInput{
QueueUrl: aws.String(qURL),
AttributeNames: []sqstypes.QueueAttributeName{sqstypes.QueueAttributeNameAll},
})
require.NoError(t, err)
assert.NotEmpty(t, r.Attributes["QueueArn"])
})
t.Run("SendMessage", func(t *testing.T) {
r, err := svc.SendMessage(ctx, &sqs.SendMessageInput{
QueueUrl: aws.String(qURL),
MessageBody: aws.String(`{"source":"go-test"}`),
})
require.NoError(t, err)
assert.NotEmpty(t, aws.ToString(r.MessageId))
})
t.Run("SendMessageBatch", func(t *testing.T) {
r, err := svc.SendMessageBatch(ctx, &sqs.SendMessageBatchInput{
QueueUrl: aws.String(qURL),
Entries: []sqstypes.SendMessageBatchRequestEntry{
{Id: aws.String("m1"), MessageBody: aws.String("batch-1")},
{Id: aws.String("m2"), MessageBody: aws.String("batch-2")},
},
})
require.NoError(t, err)
assert.Len(t, r.Successful, 2)
})
var receiptHandle *string
t.Run("ReceiveMessage", func(t *testing.T) {
r, err := svc.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
QueueUrl: aws.String(qURL),
MaxNumberOfMessages: 10,
WaitTimeSeconds: 1,
})
require.NoError(t, err)
assert.NotEmpty(t, r.Messages)
if len(r.Messages) > 0 {
receiptHandle = r.Messages[0].ReceiptHandle
}
})
t.Run("DeleteMessage", func(t *testing.T) {
require.NotNil(t, receiptHandle, "need a message to delete")
_, err := svc.DeleteMessage(ctx, &sqs.DeleteMessageInput{
QueueUrl: aws.String(qURL),
ReceiptHandle: receiptHandle,
})
require.NoError(t, err)
})
t.Run("SetQueueAttributes", func(t *testing.T) {
_, err := svc.SetQueueAttributes(ctx, &sqs.SetQueueAttributesInput{
QueueUrl: aws.String(qURL),
Attributes: map[string]string{"VisibilityTimeout": "60"},
})
require.NoError(t, err)
})
t.Run("PurgeQueue", func(t *testing.T) {
_, err := svc.PurgeQueue(ctx, &sqs.PurgeQueueInput{QueueUrl: aws.String(qURL)})
require.NoError(t, err)
})
t.Run("DeleteQueue", func(t *testing.T) {
_, err := svc.DeleteQueue(ctx, &sqs.DeleteQueueInput{QueueUrl: aws.String(qURL)})
require.NoError(t, err)
qURL = "" // Prevent double cleanup
})
}
@@ -0,0 +1,67 @@
package tests
import (
"context"
"testing"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ssm"
ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSSM(t *testing.T) {
ctx := context.Background()
svc := testutil.SSMClient()
name := "/go-test/param"
value := "go-test-value"
// Cleanup at end
t.Cleanup(func() {
svc.DeleteParameter(ctx, &ssm.DeleteParameterInput{Name: aws.String(name)})
})
t.Run("PutParameter", func(t *testing.T) {
_, err := svc.PutParameter(ctx, &ssm.PutParameterInput{
Name: aws.String(name),
Value: aws.String(value),
Type: ssmtypes.ParameterTypeString,
Overwrite: aws.Bool(true),
})
require.NoError(t, err)
})
t.Run("GetParameter", func(t *testing.T) {
r, err := svc.GetParameter(ctx, &ssm.GetParameterInput{Name: aws.String(name)})
require.NoError(t, err)
assert.Equal(t, value, aws.ToString(r.Parameter.Value))
})
t.Run("GetParameters", func(t *testing.T) {
r, err := svc.GetParameters(ctx, &ssm.GetParametersInput{Names: []string{name}})
require.NoError(t, err)
assert.Len(t, r.Parameters, 1)
})
t.Run("DescribeParameters", func(t *testing.T) {
r, err := svc.DescribeParameters(ctx, &ssm.DescribeParametersInput{})
require.NoError(t, err)
assert.NotEmpty(t, r.Parameters)
})
t.Run("GetParametersByPath", func(t *testing.T) {
r, err := svc.GetParametersByPath(ctx, &ssm.GetParametersByPathInput{
Path: aws.String("/go-test"),
})
require.NoError(t, err)
assert.NotEmpty(t, r.Parameters)
})
t.Run("DeleteParameter", func(t *testing.T) {
_, err := svc.DeleteParameter(ctx, &ssm.DeleteParameterInput{Name: aws.String(name)})
require.NoError(t, err)
})
}
@@ -0,0 +1,26 @@
package tests
import (
"context"
"testing"
"floci-sdk-test-go/internal/testutil"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSTS(t *testing.T) {
ctx := context.Background()
svc := testutil.STSClient()
t.Run("GetCallerIdentity", func(t *testing.T) {
r, err := svc.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
require.NoError(t, err)
assert.NotEmpty(t, aws.ToString(r.Account))
assert.NotEmpty(t, aws.ToString(r.Arn))
assert.NotEmpty(t, aws.ToString(r.UserId))
})
}