package tests import ( "context" "encoding/json" "fmt" "os" "strings" "time" "github.com/aws/aws-sdk-go-v2/aws" awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/sns" ) // These scenarios stand in for EventBridge, not for a client: they publish the // same "MediaConvert Job State Change" event AWS would put on the topic when a // job changes state. That is the seam cms owns — the topic, the queue, the // consumer and the record it updates. The EventBridge rule that feeds the // topic in AWS is Pulumi configuration, and is not exercised here. // // This is the only place the suite reaches for the AWS SDK. Everything a real // client does still goes over plain HTTP. const ( defaultEndpointURL = "http://localhost.localstack.cloud:4566" defaultTopicARN = "arn:aws:sns:us-east-1:000000000000:mediaconvert-job-events" defaultRegion = "us-east-1" ) func envOr(name, fallback string) string { if v := strings.TrimSpace(os.Getenv(name)); v != "" { return v } return fallback } // jobStateChange is the EventBridge event MediaConvert emits. Only the members // cms reads are modelled; AWS sends a good deal more. type jobStateChange struct { Version string `json:"version"` ID string `json:"id"` DetailType string `json:"detail-type"` Source string `json:"source"` Account string `json:"account"` Time time.Time `json:"time"` Region string `json:"region"` Resources []string `json:"resources"` Detail jobStateDetail `json:"detail"` } type jobStateDetail struct { Timestamp int64 `json:"timestamp"` AccountID string `json:"accountId"` Queue string `json:"queue"` JobID string `json:"jobId"` Status string `json:"status"` } // eventPublisher puts job state changes on the topic cms's queue subscribes to. type eventPublisher struct { sns *sns.Client topicARN string } func newEventPublisher(ctx context.Context) (*eventPublisher, error) { endpoint := envOr("AWS_ENDPOINT_URL", defaultEndpointURL) region := envOr("AWS_REGION", defaultRegion) cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region), // LocalStack accepts any credentials; these keep the SDK from hunting // for a profile that a developer's machine may not have. awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( envOr("AWS_ACCESS_KEY_ID", "test"), envOr("AWS_SECRET_ACCESS_KEY", "test"), "")), ) if err != nil { return nil, err } return &eventPublisher{ sns: sns.NewFromConfig(cfg, func(o *sns.Options) { o.BaseEndpoint = aws.String(endpoint) }), topicARN: envOr("MEDIACONVERT_EVENTS_TOPIC_ARN", defaultTopicARN), }, nil } func (p *eventPublisher) publishJobState(ctx context.Context, jobID, state string) error { event := jobStateChange{ Version: "0", ID: fmt.Sprintf("test-%d", time.Now().UnixNano()), DetailType: "MediaConvert Job State Change", Source: "aws.mediaconvert", Account: "000000000000", Time: time.Now().UTC(), Region: envOr("AWS_REGION", defaultRegion), Resources: []string{fmt.Sprintf("arn:aws:mediaconvert:%s:000000000000:jobs/%s", envOr("AWS_REGION", defaultRegion), jobID)}, Detail: jobStateDetail{ Timestamp: time.Now().UnixMilli(), AccountID: "000000000000", JobID: jobID, Status: state, }, } body, err := json.Marshal(event) if err != nil { return err } _, err = p.sns.Publish(ctx, &sns.PublishInput{ TopicArn: aws.String(p.topicARN), Message: aws.String(string(body)), }) if err != nil { return fmt.Errorf("publishing to %s: %w", p.topicARN, err) } return nil }