second first commit
This commit is contained in:
@@ -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, ¬Found), "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, ¬Found)
|
||||
|
||||
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, ¬Found)
|
||||
})
|
||||
|
||||
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, ¬Found,
|
||||
"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, ¬Found,
|
||||
"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))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user