Skip to content

Commit b34e7ea

Browse files
committed
Add SQS module integration tests for LocalStack/ElasticMQ
1 parent 9771e5f commit b34e7ea

11 files changed

Lines changed: 1201 additions & 3 deletions

File tree

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ jobs:
4141
run: |
4242
(cd redis && go build ./... && go vet ./...)
4343
(cd amqp && go build ./... && go vet ./...)
44+
# The SQS module's unit + conformance tests are network-free (fake
45+
# client), so they run here across the Go matrix; the live ElasticMQ
46+
# round-trip is `-tags=integration` in the integration job below.
47+
(cd sqs && go build ./... && go vet ./... && go test -race -count=1 ./...)
4448
4549
lint:
4650
name: Static analysis (staticcheck)
@@ -58,6 +62,7 @@ jobs:
5862
staticcheck ./...
5963
(cd redis && staticcheck ./...)
6064
(cd amqp && staticcheck ./...)
65+
(cd sqs && staticcheck ./...)
6166
6267
coverage:
6368
name: Coverage gate (>=90%)
@@ -88,6 +93,13 @@ jobs:
8893
options: >-
8994
--health-cmd "rabbitmq-diagnostics -q ping"
9095
--health-interval 10s --health-timeout 5s --health-retries 10
96+
# Free, SQS-compatible broker for the SQS transport's live round-trip.
97+
# The native image is shell-less, so readiness is polled by a TCP-probe
98+
# step below rather than a container `--health-cmd`.
99+
elasticmq:
100+
image: softwaremill/elasticmq-native:1.6.11
101+
ports:
102+
- 9324:9324
91103
steps:
92104
- uses: actions/checkout@v5
93105

@@ -107,6 +119,25 @@ jobs:
107119
AMQP_URL: amqp://guest:guest@localhost:5672/
108120
run: go test -race -count=1 ./...
109121

122+
- name: Wait for ElasticMQ
123+
run: |
124+
for i in $(seq 1 30); do
125+
if (echo > /dev/tcp/localhost/9324) >/dev/null 2>&1; then
126+
echo "ElasticMQ port 9324 open"; exit 0
127+
fi
128+
sleep 2
129+
done
130+
echo "ElasticMQ did not open port 9324"; exit 1
131+
132+
- name: Amazon SQS transport integration tests (ElasticMQ)
133+
working-directory: sqs
134+
env:
135+
SQS_ENDPOINT: http://localhost:9324
136+
AWS_REGION: us-east-1
137+
AWS_ACCESS_KEY_ID: test
138+
AWS_SECRET_ACCESS_KEY: test
139+
run: go test -tags=integration -race -count=1 ./...
140+
110141
conformance:
111142
name: Conformance suite in sync
112143
runs-on: ubuntu-latest

CHANGELOG.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
The envelope wire format is versioned separately by `meta.schema_version`
88
(currently **1**) — see the contract at [babelqueue.com](https://babelqueue.com).
99

10-
## [Unreleased]
10+
## [1.1.0] - 2026-06-12
11+
12+
The new `…/sqs` module is published as the Go submodule tag `sqs/v1.0.0`
13+
(`go get github.com/babelqueue/babelqueue-go/sqs`); the core, `redis` and `amqp`
14+
modules are unchanged at `v1.0.0`.
15+
16+
### Added
17+
- **Amazon SQS transport** — new `…/sqs` module (`github.com/babelqueue/babelqueue-go/sqs`),
18+
a `babelqueue.Transport` over the official AWS SDK (`aws-sdk-go-v2`). Implements
19+
[§3 of the broker-bindings contract](https://babelqueue.com): the canonical
20+
envelope is the `MessageBody`, projected onto native `MessageAttributes`
21+
(`bq-job`/`bq-trace-id`/`bq-message-id`/`bq-schema-version`/`bq-source-lang`/`bq-created-at`);
22+
visibility-timeout reserve → `DeleteMessage` ack; `attempts` reconciled to
23+
`ApproximateReceiveCount − 1` (never lowering a runtime-incremented count). Options:
24+
`WithRegion`, `WithEndpoint` (LocalStack/ElasticMQ), `WithQueueURLPrefix`,
25+
`WithWaitTimeSeconds`, `WithVisibilityTimeout`, `WithFIFO`, `WithMessageGroupID`,
26+
`WithContentDedup`, `WithClient`. Unit-tested against a fake SQS client (≈90%);
27+
`-tags=integration` runs a LocalStack round-trip (covers the AWS config path). The
28+
envelope is unchanged (`schema_version: 1`); SQS is purely additive. Ships as a
29+
per-SDK MINOR.
1130

1231
## [1.0.0] - 2026-06-07
1332

@@ -73,7 +92,8 @@ following the deprecation policy. The wire envelope is unchanged
7392
- Pre-1.0: the public API may change before the `1.0.0` tag.
7493
- **Zero dependencies** (standard library only); Go `>=1.21`.
7594

76-
[Unreleased]: https://github.com/BabelQueue/babelqueue-go/compare/v1.0.0...HEAD
95+
[Unreleased]: https://github.com/BabelQueue/babelqueue-go/compare/v1.1.0...HEAD
96+
[1.1.0]: https://github.com/BabelQueue/babelqueue-go/compare/v1.0.0...v1.1.0
7797
[1.0.0]: https://github.com/BabelQueue/babelqueue-go/compare/v0.2.0...v1.0.0
7898
[0.2.0]: https://github.com/BabelQueue/babelqueue-go/compare/v0.1.0...v0.2.0
7999
[0.1.0]: https://github.com/BabelQueue/babelqueue-go/releases/tag/v0.1.0

sqs/README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# babelqueue-go/sqs
2+
3+
Amazon SQS transport for the [BabelQueue Go runtime](https://github.com/babelqueue/babelqueue-go)
4+
"Polyglot Queues, Simplified."
5+
6+
It implements [§3 of the broker-bindings contract](https://babelqueue.com): the
7+
canonical envelope is the message body, and the contract fields are projected onto
8+
native SQS `MessageAttributes` so a PHP/Python/… peer can route and correlate
9+
**without parsing the body**.
10+
11+
## Install
12+
13+
```bash
14+
go get github.com/babelqueue/babelqueue-go/sqs
15+
```
16+
17+
## Use
18+
19+
```go
20+
ctx := context.Background()
21+
22+
tr, err := sqs.New(ctx,
23+
sqs.WithRegion("eu-central-1"),
24+
// sqs.WithEndpoint("http://localhost:4566"), // LocalStack/ElasticMQ
25+
)
26+
if err != nil { log.Fatal(err) }
27+
28+
app := babelqueue.NewApp(tr, babelqueue.WithDefaultQueue("orders"))
29+
30+
// produce
31+
app.Publish(ctx, "urn:babel:orders:created", map[string]any{"order_id": 1042})
32+
33+
// consume
34+
app.Handle("urn:babel:orders:created", func(ctx context.Context, env babelqueue.Envelope) error {
35+
// env.Data, env.TraceID ...
36+
return nil
37+
})
38+
app.Run(ctx)
39+
```
40+
41+
Credentials come from the standard AWS default provider chain (env vars, shared
42+
profile, instance role, IRSA, SSO). FIFO queues: add `sqs.WithFIFO(true)` (the queue
43+
name must end in `.fifo`).
44+
45+
## How it maps to the contract (§3)
46+
47+
| Envelope | SQS |
48+
| :--- | :--- |
49+
| body | `MessageBody` (byte-identical across SDKs) |
50+
| `job` (URN) | `MessageAttributes.bq-job` |
51+
| `trace_id` | `MessageAttributes.bq-trace-id` |
52+
| `meta.id` | `MessageAttributes.bq-message-id` |
53+
| `meta.schema_version` | `MessageAttributes.bq-schema-version` (Number) |
54+
| `meta.lang` | `MessageAttributes.bq-source-lang` |
55+
| `meta.created_at` | `MessageAttributes.bq-created-at` (Number, ms) |
56+
| `attempts` | reconciled to `ApproximateReceiveCount − 1` on receive |
57+
| reserve / ack | visibility timeout → `DeleteMessage` |
58+
59+
The envelope is unchanged (`schema_version` stays `1`); SQS is purely additive.
60+
61+
## Test
62+
63+
```bash
64+
go test ./... # unit (fake SQS client, no broker)
65+
go test -tags=integration ./... # integration (needs LocalStack at SQS_ENDPOINT)
66+
```
67+
68+
The default unit run fakes the AWS client. The `New` config-loading path is covered
69+
by the LocalStack integration test (`-tags=integration`).
70+
71+
## License
72+
73+
MIT

sqs/conformance_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package sqs
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/aws/aws-sdk-go-v2/aws"
10+
babelqueue "github.com/babelqueue/babelqueue-go"
11+
)
12+
13+
// conformanceDir is the core's vendored copy of the canonical suite (synced from
14+
// the conformance/ repo). The SQS-binding cases live under manifest.json → "sqs".
15+
const conformanceDir = "../testdata/conformance"
16+
17+
type sqsManifest struct {
18+
SQS struct {
19+
AttributeProjection struct {
20+
EnvelopeFile string `json:"envelope_file"`
21+
MessageAttributes map[string]goldenAttribute `json:"message_attributes"`
22+
} `json:"attribute_projection"`
23+
AttemptsReconciliation struct {
24+
Cases []reconcileCase `json:"cases"`
25+
} `json:"attempts_reconciliation"`
26+
} `json:"sqs"`
27+
}
28+
29+
type goldenAttribute struct {
30+
DataType string `json:"DataType"`
31+
StringValue string `json:"StringValue"`
32+
}
33+
34+
type reconcileCase struct {
35+
Name string `json:"name"`
36+
BodyAttempts int `json:"body_attempts"`
37+
ApproximateReceiveCount *string `json:"approximate_receive_count"`
38+
ExpectedAttempts int `json:"expected_attempts"`
39+
}
40+
41+
func TestSqsConformance(t *testing.T) {
42+
raw, err := os.ReadFile(filepath.Join(conformanceDir, "manifest.json"))
43+
if err != nil {
44+
t.Skipf("vendored conformance manifest not found: %v", err)
45+
}
46+
var m sqsManifest
47+
if err := json.Unmarshal(raw, &m); err != nil {
48+
t.Fatalf("manifest: %v", err)
49+
}
50+
51+
t.Run("attribute_projection", func(t *testing.T) {
52+
body, err := os.ReadFile(filepath.Join(conformanceDir, m.SQS.AttributeProjection.EnvelopeFile))
53+
if err != nil {
54+
t.Fatalf("fixture: %v", err)
55+
}
56+
got := attributes(string(body))
57+
want := m.SQS.AttributeProjection.MessageAttributes
58+
if len(got) != len(want) {
59+
t.Fatalf("projected %d attributes, want %d", len(got), len(want))
60+
}
61+
for key, w := range want {
62+
g, ok := got[key]
63+
if !ok {
64+
t.Errorf("missing attribute %q", key)
65+
continue
66+
}
67+
if aws.ToString(g.DataType) != w.DataType || aws.ToString(g.StringValue) != w.StringValue {
68+
t.Errorf("%s = {%s,%q}, want {%s,%q}",
69+
key, aws.ToString(g.DataType), aws.ToString(g.StringValue), w.DataType, w.StringValue)
70+
}
71+
}
72+
})
73+
74+
for _, c := range m.SQS.AttemptsReconciliation.Cases {
75+
t.Run("attempts/"+c.Name, func(t *testing.T) {
76+
env, _ := babelqueue.Make("urn:babel:orders:created", map[string]any{"x": 1})
77+
env.Attempts = c.BodyAttempts
78+
body, _ := env.Encode()
79+
80+
out := string(body)
81+
if c.ApproximateReceiveCount != nil { // absent count → no reconciliation
82+
out = reconcileAttempts(out, *c.ApproximateReceiveCount)
83+
}
84+
decoded, _ := babelqueue.Decode([]byte(out))
85+
if decoded.Attempts != c.ExpectedAttempts {
86+
t.Errorf("attempts = %d, want %d", decoded.Attempts, c.ExpectedAttempts)
87+
}
88+
})
89+
}
90+
}

sqs/go.mod

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module github.com/babelqueue/babelqueue-go/sqs
2+
3+
go 1.21
4+
5+
require (
6+
github.com/aws/aws-sdk-go-v2 v1.30.3
7+
github.com/aws/aws-sdk-go-v2/config v1.27.27
8+
github.com/aws/aws-sdk-go-v2/service/sqs v1.34.3
9+
github.com/babelqueue/babelqueue-go v1.0.0
10+
)
11+
12+
require (
13+
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
14+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
15+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
16+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
17+
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
18+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
19+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
20+
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
21+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
22+
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
23+
github.com/aws/smithy-go v1.20.3 // indirect
24+
)
25+
26+
// In-repo development: resolve the core locally. Consumers ignore replace
27+
// directives in dependencies and use the required version from the proxy.
28+
replace github.com/babelqueue/babelqueue-go => ../

sqs/go.sum

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
2+
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
3+
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
4+
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
5+
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
6+
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
7+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
8+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
9+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
10+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
11+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
12+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
13+
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
14+
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
15+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
16+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
17+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
18+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
19+
github.com/aws/aws-sdk-go-v2/service/sqs v1.34.3 h1:Vjqy5BZCOIsn4Pj8xzyqgGmsSqzz7y/WXbN3RgOoVrc=
20+
github.com/aws/aws-sdk-go-v2/service/sqs v1.34.3/go.mod h1:L0enV3GCRd5iG9B64W35C4/hwsCB00Ib+DKVGTadKHI=
21+
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
22+
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
23+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
24+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
25+
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
26+
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
27+
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
28+
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=

0 commit comments

Comments
 (0)