Skip to content

fix(sns): make Subscribe idempotent for same topic+protocol+endpoint#185

Open
naile wants to merge 1 commit intohectorvent:mainfrom
naile:fix/sns-subscribe-idempotency
Open

fix(sns): make Subscribe idempotent for same topic+protocol+endpoint#185
naile wants to merge 1 commit intohectorvent:mainfrom
naile:fix/sns-subscribe-idempotency

Conversation

@naile
Copy link
Copy Markdown

@naile naile commented Apr 3, 2026

Summary

Subscribe always created a new subscription, even when one already existed for the same TopicArn+Protocol+Endpoint. This caused duplicate subscriptions to accumulate across test re-runs, leading to duplicate message delivery.

Type of change

  • Bug fix (fix:)
  • New feature (feat:)
  • Breaking change (feat!: or fix!:)
  • Docs / chore

AWS Compatibility

Real AWS Subscribe is idempotent — calling it with the same TopicArn, Protocol, and Endpoint returns the existing subscription ARN. Verified against AWS documentation and LocalStack implementation.

There are two real AWS behaviors we don't cover:

  • Attribute conflict rejection: If you re-subscribe with different attributes (FilterPolicy, RawMessageDelivery,
    etc.), AWS throws InvalidParameterException.
  • Attribute pass-through: Subscribe accepts Attributes — floci's handlers don't parse them yet.

Both are irrelevant until we add attribute support to the Subscribe handlers.

Companion compatibility test: hectorvent/floci-compatibility-tests#38

Checklist

  • ./mvnw test passes locally
  • New or updated integration test added
  • Commit messages follow Conventional Commits

@naile naile marked this pull request as ready for review April 3, 2026 07:22
Copilot AI review requested due to automatic review settings April 3, 2026 07:22
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aligns Floci’s SNS Subscribe behavior with AWS by making it idempotent for the same TopicArn + Protocol + Endpoint, preventing duplicate subscriptions (and duplicate deliveries) across repeated runs.

Changes:

  • Add an idempotency check in SnsService.subscribe to return an existing matching subscription instead of creating a new one.
  • Add a unit test covering idempotent subscribe and distinct-endpoint behavior.
  • Add an integration test covering idempotent subscribe via the SNS Query (form-encoded) protocol.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/main/java/io/github/hectorvent/floci/services/sns/SnsService.java Adds lookup-before-create logic so Subscribe returns an existing subscription for the same topic/protocol/endpoint.
src/test/java/io/github/hectorvent/floci/services/sns/SnsServiceTest.java Adds unit tests validating idempotency and distinct subscriptions for different endpoints.
src/test/java/io/github/hectorvent/floci/services/sns/SnsIntegrationTest.java Adds query-protocol integration coverage for idempotent Subscribe and ensures no duplicate <member> entries are returned.
Comments suppressed due to low confidence (1)

src/test/java/io/github/hectorvent/floci/services/sns/SnsIntegrationTest.java:161

  • @Order(7) is now used by both subscribe_idempotent and listSubscriptionsByTopic. JUnit does not guarantee a stable ordering between tests with the same order value, which can make the suite nondeterministic as it grows. Consider giving these tests distinct order values (or removing ordering reliance if possible).
    @Test
    @Order(7)
    void subscribe_idempotent() {
        String arn = given()
            .contentType("application/x-www-form-urlencoded")
            .formParam("Action", "Subscribe")
            .formParam("TopicArn", topicArn)
            .formParam("Protocol", "sqs")
            .formParam("Endpoint", sqsQueueUrl)
        .when()
            .post("/")
        .then()
            .statusCode(200)
            .extract().xmlPath()
                .getString("SubscribeResponse.SubscribeResult.SubscriptionArn");

        assert arn.equals(subscriptionArn) : "Expected existing subscription ARN but got a new one";

        given()
            .contentType("application/x-www-form-urlencoded")
            .formParam("Action", "ListSubscriptionsByTopic")
            .formParam("TopicArn", topicArn)
        .when()
            .post("/")
        .then()
            .statusCode(200)
            .body(containsString("<member>"))
            .body(not(containsString("</member><member>")));
    }

    @Test
    @Order(7)
    void listSubscriptionsByTopic() {
        given()

AWS Subscribe returns the existing subscription when called with the
same TopicArn, Protocol, and Endpoint. Floci was always creating a new
subscription, causing duplicates to accumulate across test re-runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@naile naile force-pushed the fix/sns-subscribe-idempotency branch from c9fc4d3 to 3b45303 Compare April 3, 2026 07:33
@naile naile requested a review from Copilot April 3, 2026 07:34
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/test/java/io/github/hectorvent/floci/services/sns/SnsIntegrationTest.java:151

  • @Order(7) is now used by both subscribe_idempotent and listSubscriptionsByTopic. With OrderAnnotation, JUnit does not guarantee a deterministic execution order between tests that share the same order value, which can make the suite harder to reason about and potentially flaky as more stateful tests are added. Consider assigning a unique order and shifting subsequent order values accordingly.
    @Test
    @Order(7)
    void subscribe_idempotent() {
        String arn = given()
            .contentType("application/x-www-form-urlencoded")
            .formParam("Action", "Subscribe")
            .formParam("TopicArn", topicArn)
            .formParam("Protocol", "sqs")
            .formParam("Endpoint", sqsQueueUrl)
        .when()
            .post("/")
        .then()
            .statusCode(200)
            .extract().xmlPath()
                .getString("SubscribeResponse.SubscribeResult.SubscriptionArn");

        assert arn.equals(subscriptionArn) : "Expected existing subscription ARN but got a new one";

    }

    @Test
    @Order(7)
    void listSubscriptionsByTopic() {
        given()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants