From e90e5cafb1d86b42dd325c97bd37b4bb74f8e055 Mon Sep 17 00:00:00 2001 From: Dongie Agnir Date: Mon, 30 Mar 2026 13:39:14 -0700 Subject: [PATCH 1/2] Remove prototype files - Remove the original prototype files for the message manager in services/sns - Remove the kiro hooks not intended for release with the sns message manager --- .kiro/hooks/aws-sdk-code-review.kiro.hook | 18 - .kiro/hooks/javadoc-manual-trigger.kiro.hook | 16 - .../messagemanager/CertificateRetriever.java | 458 ---------- .../DefaultSnsMessageManager.java | 288 ------ .../messagemanager/SignatureValidator.java | 361 -------- .../messagemanager/SnsMessageParser.java | 505 ----------- .../MessageManagerConfiguration.java | 260 ------ .../SnsCertificateException.java | 79 -- .../sns/messagemanager/SnsMessage.java | 459 ---------- .../sns/messagemanager/SnsMessageManager.java | 122 --- .../SnsMessageParsingException.java | 75 -- .../SnsMessageValidationException.java | 115 --- .../SnsSignatureValidationException.java | 77 -- .../CertificateRetrieverTest.java | 828 ------------------ .../SignatureValidatorTest.java | 375 -------- .../messagemanager/SnsMessageParserTest.java | 689 --------------- .../SnsMessageManagerIntegrationTest.java | 393 --------- 17 files changed, 5118 deletions(-) delete mode 100644 .kiro/hooks/aws-sdk-code-review.kiro.hook delete mode 100644 .kiro/hooks/javadoc-manual-trigger.kiro.hook delete mode 100644 services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/CertificateRetriever.java delete mode 100644 services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/DefaultSnsMessageManager.java delete mode 100644 services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/SignatureValidator.java delete mode 100644 services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/SnsMessageParser.java delete mode 100644 services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/MessageManagerConfiguration.java delete mode 100644 services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsCertificateException.java delete mode 100644 services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessage.java delete mode 100644 services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageManager.java delete mode 100644 services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageParsingException.java delete mode 100644 services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageValidationException.java delete mode 100644 services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsSignatureValidationException.java delete mode 100644 services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/CertificateRetrieverTest.java delete mode 100644 services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/SignatureValidatorTest.java delete mode 100644 services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/SnsMessageParserTest.java delete mode 100644 services/sns/src/test/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageManagerIntegrationTest.java diff --git a/.kiro/hooks/aws-sdk-code-review.kiro.hook b/.kiro/hooks/aws-sdk-code-review.kiro.hook deleted file mode 100644 index 729dc463e5ed..000000000000 --- a/.kiro/hooks/aws-sdk-code-review.kiro.hook +++ /dev/null @@ -1,18 +0,0 @@ -{ - "enabled": true, - "name": "AWS SDK Java v2 Code Review", - "description": "Performs manual code review leveraging all steering documentation to verify code against AWS SDK Java v2 guidelines, best practices, and architectural standards", - "version": "1", - "when": { - "type": "userTriggered", - "patterns": [ - "**/*.java", - "**/*.md", - "**/*.json" - ] - }, - "then": { - "type": "askAgent", - "prompt": "# AWS SDK Java v2 Code Review\n\nYou are reviewing code for the AWS SDK for Java v2 project. Use the steering documentation in `.kiro/steering/` to apply context-specific guidelines based on file patterns.\n\n## Review Scope\nAnalyze the **changed files** in the current workspace and apply appropriate guidelines based on file types using the steering documentation.\n\n## Guidelines Application\nLoad and apply guidelines from the steering documentation based on file patterns:\n\n- `.kiro/steering/aws-sdk-java-v2-general.md` - For all `**/*.java` files\n- `.kiro/steering/logging-guidelines.md` - For all `**/*.java` files \n- `.kiro/steering/client-configuration-guidelines.md` - For `**/*{Config,Configuration,Builder}*.java`\n- `.kiro/steering/async-programming-guidelines.md` - For `**/*{Async,CompletableFuture}*.java`\n- `.kiro/steering/reactive-streams-guidelines.md` - For `**/*{Publisher,Subscriber}*.java`\n- `.kiro/steering/testing-guidelines.md` - For `**/{test,it}/**/*.java`\n- `.kiro/steering/javadoc-guidelines.md` - For `**/src/main/**/*.java`\n- `.kiro/steering/code-generation-guidelines.md` - For `{codegen/**/*.java,**/poet/**/*.java}`\n\n## Review Process\n1. **Load relevant steering docs** for each changed file based on its path pattern\n2. **Apply all applicable guidelines** from the loaded documentation\n3. **Categorize findings** by severity:\n - āŒ **CRITICAL**: Must fix before merge (violations of MUST requirements)\n - āš ļø **GUIDELINE**: Should fix for consistency (violations of SHOULD requirements)\n - šŸ’” **SUGGESTION**: Consider for improvement\n - āœ… **COMPLIANT**: Follows guidelines correctly\n\n## Output Format\n\nFor each file reviewed, provide:\nšŸ“ [File Path]\nType: [Configuration/Async/Test/General/etc.] Compliance: [Percentage]%\n\nIssues Found:\n- āŒ CRITICAL: [Issue description]\n - Guideline: [Reference to specific steering doc section]\n - Fix: [Specific suggestion]\n\n- āš ļø GUIDELINE: [Issue description]\n - Reference: [Steering doc reference]\n - Suggestion: [How to improve]\n-šŸ’” SUGGESTION: [Improvement opportunity]\n\n## Summary Report\n- **Changed Files Reviewed**: X\n- **Guidelines Applied**: [List of steering docs used]\n- **Overall Compliance**: X%\n- **Critical Issues**: X (must fix before merge)\n- **Guideline Violations**: X (should fix for consistency)\n- **Ready for Review**: [Yes/No based on critical issues]\n\nProvide actionable feedback based on the comprehensive guidelines in the steering documentation, helping developers understand what to fix and why it matters for the AWS SDK project.\n" - } -} \ No newline at end of file diff --git a/.kiro/hooks/javadoc-manual-trigger.kiro.hook b/.kiro/hooks/javadoc-manual-trigger.kiro.hook deleted file mode 100644 index 52b63a62e6da..000000000000 --- a/.kiro/hooks/javadoc-manual-trigger.kiro.hook +++ /dev/null @@ -1,16 +0,0 @@ -{ - "enabled": false, - "name": "Manual Javadoc Generator", - "description": "A manual trigger to add comprehensive Javadoc documentation for Java files following the javadoc-guidelines.md standards", - "version": "1", - "when": { - "type": "fileEdited", - "patterns": [ - "**/*.java" - ] - }, - "then": { - "type": "askAgent", - "prompt": "Please add comprehensive Javadoc documentation to the Java files that have been modified. Follow the guidelines specified in javadoc-guidelines.md. Focus on:\n\n1. Class-level documentation explaining the purpose and usage\n2. Method documentation with @param, @return, and @throws tags where appropriate\n3. Field documentation for public/protected fields\n4. Code examples where helpful\n5. Proper formatting and professional tone\n\nEnsure the documentation is clear, concise, and follows Java documentation best practices as outlined in the guidelines." - } -} \ No newline at end of file diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/CertificateRetriever.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/CertificateRetriever.java deleted file mode 100644 index 164de17976cf..000000000000 --- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/CertificateRetriever.java +++ /dev/null @@ -1,458 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.services.sns.internal.messagemanager; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.time.Instant; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.regex.Pattern; -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.http.HttpExecuteRequest; -import software.amazon.awssdk.http.HttpExecuteResponse; -import software.amazon.awssdk.http.SdkHttpClient; -import software.amazon.awssdk.http.SdkHttpMethod; -import software.amazon.awssdk.http.SdkHttpRequest; -import software.amazon.awssdk.services.sns.messagemanager.SnsCertificateException; -import software.amazon.awssdk.utils.StringUtils; -import software.amazon.awssdk.utils.Validate; - -/** - * Internal certificate retriever for SNS message validation. - * - *

This class handles secure retrieval and caching of SNS signing certificates from AWS. - * It implements comprehensive security validations to ensure certificate authenticity and - * prevent various attack vectors including certificate spoofing and man-in-the-middle attacks. - * - *

Security Features: - *

- * - *

Trusted Domains: - * The retriever only accepts certificates from pre-validated SNS domains including: - *

- * - *

Thread Safety: - * This class is thread-safe and can be used concurrently from multiple threads. - * Certificate caching is implemented using thread-safe collections. - * - *

Usage: - * This class is intended for internal use by the SNS message manager and should not be - * used directly by client code. Certificates are automatically retrieved and cached - * during message signature validation. - * - * @see SignatureValidator - * @see DefaultSnsMessageManager - */ -@SdkInternalApi -public final class CertificateRetriever { - - // Trusted SNS domain patterns for different AWS partitions - private static final Pattern[] TRUSTED_SNS_DOMAIN_PATTERNS = { - // AWS Standard partition: sns..amazonaws.com - Pattern.compile("^sns\\.[a-z0-9][a-z0-9\\-]*[a-z0-9]\\.amazonaws\\.com$"), - - // AWS GovCloud partition: sns.us-gov-.amazonaws.com - Pattern.compile("^sns\\.us-gov-[a-z0-9][a-z0-9\\-]*[a-z0-9]\\.amazonaws\\.com$"), - - // AWS China partition: sns.cn-.amazonaws.com.cn - Pattern.compile("^sns\\.cn-[a-z0-9][a-z0-9\\-]*[a-z0-9]\\.amazonaws\\.com\\.cn$") - }; - - private static final String HTTPS_SCHEME = "https"; - private static final int MAX_CERTIFICATE_SIZE = 10 * 1024; // 10KB max certificate size - private static final Duration DEFAULT_HTTP_TIMEOUT = Duration.ofSeconds(10); - - private final SdkHttpClient httpClient; - private final Duration certificateCacheTimeout; - private final ConcurrentMap certificateCache; - - /** - * Creates a new certificate retriever with the specified configuration. - * - * @param httpClient The HTTP client to use for certificate retrieval. - * @param certificateCacheTimeout The cache timeout for certificates. - * @throws NullPointerException If httpClient or certificateCacheTimeout is null. - */ - public CertificateRetriever(SdkHttpClient httpClient, Duration certificateCacheTimeout) { - this.httpClient = Validate.paramNotNull(httpClient, "httpClient"); - this.certificateCacheTimeout = Validate.paramNotNull(certificateCacheTimeout, "certificateCacheTimeout"); - this.certificateCache = new ConcurrentHashMap<>(); - } - - /** - * Retrieves a certificate from the specified URL with security validation. - *

- * This method performs comprehensive security checks: - *

- * - * @param certificateUrl The URL of the certificate to retrieve. - * @return The certificate bytes. - * @throws SnsCertificateException If certificate retrieval or validation fails. - * @throws NullPointerException If certificateUrl is null. - */ - public byte[] retrieveCertificate(String certificateUrl) { - Validate.paramNotNull(certificateUrl, "certificateUrl"); - - // Check cache first - CachedCertificate cached = certificateCache.get(certificateUrl); - if (cached != null && !cached.isExpired()) { - return cached.getCertificateBytes(); - } - - // Validate certificate URL security - validateCertificateUrl(certificateUrl); - - // Retrieve certificate from AWS - byte[] certificateBytes = fetchCertificateFromUrl(certificateUrl); - - // Cache the certificate - certificateCache.put(certificateUrl, new CachedCertificate(certificateBytes, certificateCacheTimeout)); - - return certificateBytes; - } - - /** - * Validates that the certificate URL is from a trusted SNS domain and uses HTTPS. - * - * @param certificateUrl The certificate URL to validate. - * @throws SnsCertificateException If the URL is not trusted or secure. - */ - private void validateCertificateUrl(String certificateUrl) { - if (StringUtils.isBlank(certificateUrl)) { - throw SnsCertificateException.builder() - .message("Certificate URL cannot be null or empty") - .build(); - } - - URI uri; - try { - uri = new URI(certificateUrl); - } catch (URISyntaxException e) { - throw SnsCertificateException.builder() - .message("Invalid certificate URL format: " + certificateUrl) - .cause(e) - .build(); - } - - // Ensure HTTPS only - if (!HTTPS_SCHEME.equalsIgnoreCase(uri.getScheme())) { - throw SnsCertificateException.builder() - .message("Certificate URL must use HTTPS. Provided URL: " + certificateUrl) - .build(); - } - - // Validate against trusted SNS domain patterns - String host = uri.getHost(); - if (host == null || !isTrustedSnsDomain(host)) { - throw SnsCertificateException.builder() - .message("Certificate URL is not from a trusted SNS domain. Host: " + host + - ". Expected format: sns..amazonaws.com, sns.us-gov-.amazonaws.com, " + - "or sns.cn-.amazonaws.com.cn") - .build(); - } - } - - /** - * Checks if the given host is a trusted SNS domain using pattern matching. - *

- * This method validates against known AWS SNS domain patterns for all partitions: - *

    - *
  • AWS Standard: sns.<region>.amazonaws.com
  • - *
  • AWS GovCloud: sns.us-gov-<region>.amazonaws.com
  • - *
  • AWS China: sns.cn-<region>.amazonaws.com.cn
  • - *
- *

- * The patterns ensure that: - *

    - *
  • Only valid region names are accepted (alphanumeric and hyphens, not starting/ending with hyphen)
  • - *
  • The domain structure matches AWS SNS certificate hosting patterns
  • - *
  • New regions are automatically supported without code changes
  • - *
- * - * @param host The host to check. - * @return true if the host matches a trusted SNS domain pattern, false otherwise. - */ - private boolean isTrustedSnsDomain(String host) { - if (host == null) { - return false; - } - - // Convert to lowercase for case-insensitive matching - String normalizedHost = host.toLowerCase(); - - // Check against all trusted SNS domain patterns - for (Pattern pattern : TRUSTED_SNS_DOMAIN_PATTERNS) { - if (pattern.matcher(normalizedHost).matches()) { - return true; - } - } - - return false; - } - - /** - * Fetches the certificate from the specified URL. - * - * @param certificateUrl The URL to fetch the certificate from. - * @return The certificate bytes. - * @throws SnsCertificateException If certificate retrieval fails. - */ - private byte[] fetchCertificateFromUrl(String certificateUrl) { - SdkHttpRequest httpRequest = SdkHttpRequest.builder() - .method(SdkHttpMethod.GET) - .uri(URI.create(certificateUrl)) - .build(); - - HttpExecuteRequest executeRequest = HttpExecuteRequest.builder() - .request(httpRequest) - .build(); - - try { - HttpExecuteResponse response = httpClient.prepareRequest(executeRequest).call(); - - if (!response.httpResponse().isSuccessful()) { - throw SnsCertificateException.builder() - .message("Failed to retrieve certificate from URL: " + certificateUrl + - ". HTTP status: " + response.httpResponse().statusCode()) - .build(); - } - - return readCertificateBytes(response); - - } catch (IOException e) { - throw SnsCertificateException.builder() - .message("IO error while retrieving certificate from URL: " + certificateUrl) - .cause(e) - .build(); - } catch (Exception e) { - throw SnsCertificateException.builder() - .message("Unexpected error while retrieving certificate from URL: " + certificateUrl) - .cause(e) - .build(); - } - } - - /** - * Reads certificate bytes from the HTTP response with comprehensive validation. - * - * @param response The HTTP response containing the certificate. - * @return The certificate bytes. - * @throws IOException If reading fails. - * @throws SnsCertificateException If certificate validation fails. - */ - private byte[] readCertificateBytes(HttpExecuteResponse response) throws IOException { - try (InputStream inputStream = response.responseBody().orElseThrow( - () -> SnsCertificateException.builder() - .message("Certificate response body is empty") - .build())) { - - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - byte[] chunk = new byte[1024]; - int totalBytesRead = 0; - int bytesRead; - - while ((bytesRead = inputStream.read(chunk)) != -1) { - totalBytesRead += bytesRead; - - // Protect against oversized certificates - if (totalBytesRead > MAX_CERTIFICATE_SIZE) { - throw SnsCertificateException.builder() - .message("Certificate size exceeds maximum allowed size of " + MAX_CERTIFICATE_SIZE + " bytes") - .build(); - } - - buffer.write(chunk, 0, bytesRead); - } - - byte[] certificateBytes = buffer.toByteArray(); - - if (certificateBytes.length == 0) { - throw SnsCertificateException.builder() - .message("Retrieved certificate is empty") - .build(); - } - - // Perform additional security validation on certificate content - validateCertificateContent(certificateBytes); - - return certificateBytes; - } - } - - /** - * Validates the certificate content for security compliance. - *

- * This method performs additional security checks on the certificate content - * to ensure it meets security requirements and is not malformed or malicious. - * - * @param certificateBytes The certificate bytes to validate. - * @throws SnsCertificateException If certificate content validation fails. - */ - private void validateCertificateContent(byte[] certificateBytes) { - // Check for minimum certificate size (too small indicates potential issues) - if (certificateBytes.length < 100) { - throw SnsCertificateException.builder() - .message("Certificate is too small (" + certificateBytes.length + " bytes). " + - "Valid X.509 certificates should be at least 100 bytes") - .build(); - } - - // Validate certificate starts with expected X.509 PEM or DER format markers - if (!isValidCertificateFormat(certificateBytes)) { - throw SnsCertificateException.builder() - .message("Certificate does not appear to be in valid X.509 PEM or DER format") - .build(); - } - - // Check for suspicious content patterns that might indicate tampering - validateCertificateIntegrity(certificateBytes); - } - - /** - * Validates that the certificate is in a recognized X.509 format. - * - * @param certificateBytes The certificate bytes to check. - * @return true if the format appears valid, false otherwise. - */ - private boolean isValidCertificateFormat(byte[] certificateBytes) { - if (certificateBytes.length < 10) { - return false; - } - - // Check for PEM format (starts with "-----BEGIN CERTIFICATE-----") - String beginPem = "-----BEGIN CERTIFICATE-----"; - if (certificateBytes.length >= beginPem.length()) { - String start = new String(certificateBytes, 0, beginPem.length(), StandardCharsets.US_ASCII); - if (beginPem.equals(start)) { - return true; - } - } - - // Check for DER format (starts with ASN.1 SEQUENCE tag 0x30) - if (certificateBytes[0] == 0x30) { - // Basic DER validation - second byte should indicate length encoding - if (certificateBytes.length > 1) { - byte lengthByte = certificateBytes[1]; - // Length byte should be reasonable for certificate size - return (lengthByte & 0x80) == 0 || (lengthByte & 0x7F) <= 4; - } - } - - return false; - } - - /** - * Validates certificate integrity by checking for suspicious patterns. - * - * @param certificateBytes The certificate bytes to validate. - * @throws SnsCertificateException If suspicious patterns are detected. - */ - private void validateCertificateIntegrity(byte[] certificateBytes) { - // Check for excessive null bytes which might indicate padding attacks - int nullByteCount = 0; - int consecutiveNullBytes = 0; - int maxConsecutiveNullBytes = 0; - - for (byte b : certificateBytes) { - if (b == 0) { - nullByteCount++; - consecutiveNullBytes++; - maxConsecutiveNullBytes = Math.max(maxConsecutiveNullBytes, consecutiveNullBytes); - } else { - consecutiveNullBytes = 0; - } - } - - // If more than 10% of the certificate is null bytes, it's suspicious - if (nullByteCount > certificateBytes.length * 0.1) { - throw SnsCertificateException.builder() - .message("Certificate contains excessive null bytes (" + nullByteCount + " out of " + - certificateBytes.length + "), which may indicate tampering") - .build(); - } - - // If there are more than 50 consecutive null bytes, it's suspicious - if (maxConsecutiveNullBytes > 50) { - throw SnsCertificateException.builder() - .message("Certificate contains " + maxConsecutiveNullBytes + - " consecutive null bytes, which may indicate tampering") - .build(); - } - } - - /** - * Clears the certificate cache. - *

- * This method is primarily intended for testing purposes. - */ - void clearCache() { - certificateCache.clear(); - } - - /** - * Returns the current cache size. - *

- * This method is primarily intended for testing purposes. - * - * @return The number of cached certificates. - */ - int getCacheSize() { - return certificateCache.size(); - } - - /** - * Cached certificate with expiration time. - */ - private static final class CachedCertificate { - private final byte[] certificateBytes; - private final Instant expirationTime; - - CachedCertificate(byte[] certificateBytes, Duration cacheTimeout) { - this.certificateBytes = certificateBytes.clone(); - this.expirationTime = Instant.now().plus(cacheTimeout); - } - - byte[] getCertificateBytes() { - return certificateBytes.clone(); - } - - boolean isExpired() { - return Instant.now().isAfter(expirationTime); - } - } -} \ No newline at end of file diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/DefaultSnsMessageManager.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/DefaultSnsMessageManager.java deleted file mode 100644 index 871e9126ab53..000000000000 --- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/DefaultSnsMessageManager.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.services.sns.internal.messagemanager; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.http.SdkHttpClient; -import software.amazon.awssdk.http.SdkHttpConfigurationOption; -import software.amazon.awssdk.services.sns.messagemanager.MessageManagerConfiguration; -import software.amazon.awssdk.services.sns.messagemanager.SnsCertificateException; -import software.amazon.awssdk.services.sns.messagemanager.SnsMessage; -import software.amazon.awssdk.services.sns.messagemanager.SnsMessageManager; -import software.amazon.awssdk.services.sns.messagemanager.SnsMessageParsingException; -import software.amazon.awssdk.services.sns.messagemanager.SnsSignatureValidationException; -import software.amazon.awssdk.utils.AttributeMap; - - -/** - * Default implementation of {@link SnsMessageManager} that provides comprehensive SNS message validation. - * - *

This class coordinates between the message parser, signature validator, and certificate retriever - * to provide complete SNS message validation functionality. It handles the entire validation pipeline - * including JSON parsing, certificate retrieval and caching, and cryptographic signature verification. - * - *

The implementation supports: - *

    - *
  • Both SignatureVersion1 (SHA1) and SignatureVersion2 (SHA256) signature algorithms
  • - *
  • Automatic certificate retrieval and caching from trusted SNS domains
  • - *
  • All SNS message types (Notification, SubscriptionConfirmation, UnsubscribeConfirmation)
  • - *
  • Configurable HTTP client and certificate cache timeout settings
  • - *
  • Thread-safe concurrent usage
  • - *
- * - *

This class manages the lifecycle of HTTP resources and implements {@link SdkAutoCloseable} - * to ensure proper cleanup. When using a custom HTTP client via configuration, the client's - * lifecycle is managed externally. When using the default HTTP client, this class manages - * the client's lifecycle and closes it when {@link #close()} is called. - * - *

Thread Safety: This class is thread-safe and can be used concurrently - * from multiple threads. Certificate caching is implemented using thread-safe collections. - * - *

Resource Management: Instances should be closed when no longer needed - * to free HTTP client resources. Use try-with-resources or explicit close() calls. - * - * @see SnsMessageManager - * @see MessageManagerConfiguration - * @see SnsMessage - */ -@SdkInternalApi -public final class DefaultSnsMessageManager implements SnsMessageManager { - - /** The configuration settings for this message manager instance. */ - private final MessageManagerConfiguration configuration; - - /** Certificate retriever for fetching and caching SNS signing certificates. */ - private final CertificateRetriever certificateRetriever; - - /** HTTP client used for certificate retrieval operations. */ - private final SdkHttpClient httpClient; - - /** Flag indicating whether this instance should close the HTTP client on cleanup. */ - private final boolean shouldCloseHttpClient; - - private DefaultSnsMessageManager(DefaultBuilder builder) { - this.configuration = builder.configuration != null - ? builder.configuration - : MessageManagerConfiguration.builder().build(); - - // Initialize HTTP client - use provided one or create default - if (configuration.httpClient() != null) { - this.httpClient = configuration.httpClient(); - this.shouldCloseHttpClient = false; - } else { - this.httpClient = null; - this.shouldCloseHttpClient = true; - } - - // Initialize certificate retriever - this.certificateRetriever = new CertificateRetriever(httpClient, configuration.certificateCacheTimeout()); - } - - /** - * Creates a new builder for {@link DefaultSnsMessageManager}. - * - * @return A new builder instance. - */ - public static Builder builder() { - return new DefaultBuilder(); - } - - @Override - public SnsMessage parseMessage(InputStream messageStream) { - // Comprehensive input validation - validateInputStreamParameter(messageStream); - - try { - String messageContent = readInputStreamToString(messageStream); - return parseMessage(messageContent); - } catch (IOException e) { - throw SnsMessageParsingException.builder() - .message("Failed to read message from InputStream. This may indicate a network issue, " + - "stream corruption, or insufficient memory. Error: " + e.getMessage()) - .cause(e) - .build(); - } - } - - @Override - public SnsMessage parseMessage(String messageContent) { - // Comprehensive input validation with detailed error messages - validateStringMessageParameter(messageContent); - - try { - // Step 1: Parse the JSON message - SnsMessage parsedMessage = SnsMessageParser.parseMessage(messageContent); - - // Step 2: Retrieve the certificate - byte[] certificateBytes = certificateRetriever.retrieveCertificate(parsedMessage.signingCertUrl()); - - // Step 3: Validate the signature - SignatureValidator.validateSignature(parsedMessage, certificateBytes); - - // Return the validated message - return parsedMessage; - - } catch (SnsMessageParsingException | SnsSignatureValidationException | SnsCertificateException e) { - // Let SNS-specific exceptions propagate as-is with their original detailed messages - throw e; - } catch (Exception e) { - // Only wrap truly unexpected exceptions - throw SnsMessageParsingException.builder() - .message("Unexpected error during message validation: " + e.getMessage() + - ". Please check that the message is a valid SNS message and try again.") - .cause(e) - .build(); - } - } - - @Override - public void close() { - // Close HTTP client only if we created it - if (shouldCloseHttpClient && httpClient != null) { - try { - httpClient.close(); - } catch (Exception e) { - // Log and ignore - we're closing anyway - // In a real implementation, this would use a logger - } - } - } - - /** - * Validates the InputStream parameter with comprehensive error reporting. - * - * @param messageStream The InputStream to validate. - * @throws SnsMessageParsingException If validation fails. - */ - private void validateInputStreamParameter(InputStream messageStream) { - if (messageStream == null) { - throw SnsMessageParsingException.builder() - .message("Message InputStream cannot be null. Please provide a valid InputStream containing SNS message data.") - .build(); - } - - // Additional validation could be added here for stream state if needed - } - - /** - * Validates the String message parameter with comprehensive error reporting. - * - * @param messageContent The message content to validate. - * @throws SnsMessageParsingException If validation fails. - */ - private void validateStringMessageParameter(String messageContent) { - if (messageContent == null) { - throw SnsMessageParsingException.builder() - .message("Message content cannot be null. Please provide a valid SNS message JSON string.") - .build(); - } - - if (messageContent.trim().isEmpty()) { - throw SnsMessageParsingException.builder() - .message("Message content cannot be empty or contain only whitespace. " + - "Please provide a valid SNS message JSON string.") - .build(); - } - - // Check for reasonable message size limits - if (messageContent.length() > 256 * 1024) { // 256KB limit - throw SnsMessageParsingException.builder() - .message("Message content is too large (" + messageContent.length() + " characters). " + - "SNS messages should typically be under 256KB. Please verify the message content.") - .build(); - } - - // Basic format validation - should look like JSON - String trimmed = messageContent.trim(); - if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) { - throw SnsMessageParsingException.builder() - .message("Message content does not appear to be valid JSON. " + - "SNS messages must be in JSON format starting with '{' and ending with '}'. " + - "Received content starts with: " + - (trimmed.length() > 50 ? trimmed.substring(0, 50) + "..." : trimmed)) - .build(); - } - } - - /** - * Reads an InputStream to a String using UTF-8 encoding with enhanced error handling. - * - * @param inputStream The InputStream to read. - * @return The string content. - * @throws IOException If reading fails. - */ - private String readInputStreamToString(InputStream inputStream) throws IOException { - try (ByteArrayOutputStream result = new ByteArrayOutputStream()) { - // Read with size limit to prevent memory exhaustion - byte[] buffer = new byte[8192]; - int totalBytesRead = 0; - int maxSize = 256 * 1024; // 256KB limit - int bytesRead; - - while ((bytesRead = inputStream.read(buffer)) != -1) { - totalBytesRead += bytesRead; - - if (totalBytesRead > maxSize) { - throw new IOException("InputStream content exceeds maximum allowed size of " + maxSize + " bytes. " + - "SNS messages should typically be much smaller."); - } - - result.write(buffer, 0, bytesRead); - } - - if (totalBytesRead == 0) { - throw new IOException("InputStream is empty. Please provide a valid InputStream containing SNS message data."); - } - - return result.toString(StandardCharsets.UTF_8.name()); - } - } - - /** - * Creates HTTP defaults for the SNS message manager. - */ - private static AttributeMap createHttpDefaults() { - return AttributeMap.builder() - .put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, java.time.Duration.ofSeconds(10)) - .put(SdkHttpConfigurationOption.READ_TIMEOUT, java.time.Duration.ofSeconds(30)) - .build(); - } - - /** - * Builder implementation for {@link DefaultSnsMessageManager}. - */ - public static final class DefaultBuilder implements Builder { - private MessageManagerConfiguration configuration; - - private DefaultBuilder() { - } - - @Override - public Builder configuration(MessageManagerConfiguration configuration) { - this.configuration = configuration; - return this; - } - - @Override - public SnsMessageManager build() { - return new DefaultSnsMessageManager(this); - } - } -} \ No newline at end of file diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/SignatureValidator.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/SignatureValidator.java deleted file mode 100644 index 742b73bf50d0..000000000000 --- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/SignatureValidator.java +++ /dev/null @@ -1,361 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.services.sns.internal.messagemanager; - -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.Signature; -import java.security.SignatureException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Base64; -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.services.sns.messagemanager.SnsCertificateException; -import software.amazon.awssdk.services.sns.messagemanager.SnsMessage; -import software.amazon.awssdk.services.sns.messagemanager.SnsSignatureValidationException; -import software.amazon.awssdk.utils.Validate; - -/** - * Internal validator for SNS message signatures. - * - *

This class handles cryptographic verification of SNS message signatures using AWS certificates. - * It supports both SignatureVersion1 (SHA1) and SignatureVersion2 (SHA256) as per AWS SNS standards, - * ensuring that messages are genuinely from Amazon SNS and have not been tampered with during transmission. - * - *

The validator performs comprehensive signature verification including: - *

    - *
  • Certificate validation and chain of trust verification
  • - *
  • Signature algorithm selection based on signature version
  • - *
  • Message canonicalization for signature verification
  • - *
  • Cryptographic signature verification using public key
  • - *
  • Certificate key usage validation for digital signatures
  • - *
- * - *

Security Features: - *

    - *
  • Validates certificate issuer against known Amazon SNS certificate authorities
  • - *
  • Checks certificate validity period and expiration
  • - *
  • Verifies certificate subject contains appropriate SNS identifiers
  • - *
  • Ensures certificates have digital signature key usage enabled
  • - *
  • Supports multiple AWS partitions (aws, aws-gov, aws-cn)
  • - *
- * - *

Thread Safety: This class is thread-safe as all methods are static - * and do not maintain any mutable state. - * - *

Usage: This class is intended for internal use by the SNS message manager - * and should not be used directly by client code. Signature validation is automatically - * performed during message parsing through {@link DefaultSnsMessageManager}. - * - * @see DefaultSnsMessageManager - * @see CertificateRetriever - * @see SnsMessage - */ -@SdkInternalApi -public final class SignatureValidator { - - private static final String SIGNATURE_VERSION_1 = "1"; - private static final String SIGNATURE_VERSION_2 = "2"; - - private static final String SHA1_WITH_RSA = "SHA1withRSA"; - private static final String SHA256_WITH_RSA = "SHA256withRSA"; - - private static final String CERTIFICATE_TYPE = "X.509"; - - private SignatureValidator() { - // Utility class - prevent instantiation - } - - /** - * Validates the signature of an SNS message using the provided certificate. - * - *

This method performs comprehensive cryptographic verification of the SNS message signature - * to ensure the message is authentic and from Amazon SNS. The validation process includes: - *

    - *
  • Parsing and validating the X.509 certificate
  • - *
  • Verifying certificate validity period and chain of trust
  • - *
  • Checking certificate key usage for digital signatures
  • - *
  • Building canonical message string for signature verification
  • - *
  • Performing cryptographic signature verification using the certificate's public key
  • - *
- * - *

The method supports both SignatureVersion1 (SHA1withRSA) and SignatureVersion2 (SHA256withRSA) - * signature algorithms as specified by AWS SNS standards. - * - *

Security Validation: - *

    - *
  • Certificate must be issued by a trusted Amazon SNS certificate authority
  • - *
  • Certificate must be within its validity period
  • - *
  • Certificate subject must contain appropriate SNS identifiers
  • - *
  • Certificate must have digital signature key usage enabled
  • - *
- * - * @param message The SNS message to validate. Must contain all required signature fields - * including signature, signatureVersion, and message content. - * @param certificateBytes The X.509 certificate bytes in PEM or DER format used for - * signature verification. Must be a valid certificate from Amazon SNS. - * @throws SnsSignatureValidationException If signature verification fails, indicating the - * message may have been tampered with or is not from Amazon SNS - * @throws SnsCertificateException If certificate processing, parsing, or validation fails - * @throws NullPointerException If message or certificateBytes is null - */ - public static void validateSignature(SnsMessage message, byte[] certificateBytes) { - Validate.paramNotNull(message, "message"); - Validate.paramNotNull(certificateBytes, "certificateBytes"); - - X509Certificate certificate = parseCertificate(certificateBytes); - validateCertificate(certificate); - - String signatureAlgorithm = getSignatureAlgorithm(message.signatureVersion()); - String canonicalMessage = buildCanonicalMessage(message); - - verifySignature(message.signature(), canonicalMessage, certificate.getPublicKey(), signatureAlgorithm); - } - - private static X509Certificate parseCertificate(byte[] certificateBytes) { - try { - CertificateFactory certificateFactory = CertificateFactory.getInstance(CERTIFICATE_TYPE); - Certificate certificate = certificateFactory.generateCertificate( - new ByteArrayInputStream(certificateBytes)); - - if (!(certificate instanceof X509Certificate)) { - throw SnsCertificateException.builder() - .message("Certificate is not an X.509 certificate") - .build(); - } - - return (X509Certificate) certificate; - } catch (CertificateException e) { - throw SnsCertificateException.builder() - .message("Failed to parse certificate: " + e.getMessage()) - .cause(e) - .build(); - } - } - - private static void validateCertificate(X509Certificate certificate) { - try { - // Check certificate validity period - certificate.checkValidity(); - - // Verify certificate is issued by Amazon SNS with comprehensive chain validation - validateCertificateChainOfTrust(certificate); - - // Additional security checks - validateCertificateKeyUsage(certificate); - - } catch (CertificateException e) { - throw SnsCertificateException.builder() - .message("Certificate validation failed: " + e.getMessage()) - .cause(e) - .build(); - } - } - - /** - * Validates the certificate chain of trust to ensure it's issued by Amazon SNS. - *

- * This method performs comprehensive validation of the certificate issuer to ensure - * it comes from a trusted Amazon SNS certificate authority. It checks multiple - * issuer patterns to support different AWS partitions and certificate structures. - * - * @param certificate The certificate to validate. - * @throws SnsCertificateException If the certificate is not from a trusted Amazon SNS issuer. - */ - private static void validateCertificateChainOfTrust(X509Certificate certificate) { - String issuerDN = certificate.getIssuerDN().getName(); - String subjectDN = certificate.getSubjectDN().getName(); - - if (!isAmazonSnsIssuer(issuerDN)) { - throw SnsCertificateException.builder() - .message("Certificate is not issued by Amazon SNS. Issuer: " + issuerDN + - ". Expected issuer patterns: CN=sns.amazonaws.com, CN=Amazon, " + - "O=Amazon.com Inc., or O=Amazon Web Services") - .build(); - } - - // Additional validation for subject DN to ensure it's an SNS certificate - if (!isValidSnsSubject(subjectDN)) { - throw SnsCertificateException.builder() - .message("Certificate subject is not valid for Amazon SNS. Subject: " + subjectDN + - ". Expected subject patterns should contain sns.amazonaws.com or Amazon SNS identifiers") - .build(); - } - } - - /** - * Validates certificate key usage to ensure it's appropriate for signature verification. - *

- * This method checks that the certificate has the appropriate key usage extensions - * for digital signature verification, which is required for SNS message validation. - * - * @param certificate The certificate to validate. - * @throws SnsCertificateException If the certificate doesn't have appropriate key usage. - */ - private static void validateCertificateKeyUsage(X509Certificate certificate) { - boolean[] keyUsage = certificate.getKeyUsage(); - - // Key usage array indices according to RFC 5280: - // 0: digitalSignature, 1: nonRepudiation, 2: keyEncipherment, etc. - if (keyUsage != null && keyUsage.length > 0) { - // Check if digital signature is enabled (index 0) - if (!keyUsage[0]) { - throw SnsCertificateException.builder() - .message("Certificate does not have digital signature key usage enabled, " + - "which is required for SNS message signature verification") - .build(); - } - } - // If keyUsage is null, the certificate doesn't restrict key usage, which is acceptable - } - - private static boolean isAmazonSnsIssuer(String issuerDN) { - if (issuerDN == null) { - return false; - } - - // Convert to lowercase for case-insensitive matching - String normalizedIssuer = issuerDN.toLowerCase(); - - // Check for various Amazon SNS certificate issuer patterns - return normalizedIssuer.contains("cn=sns.amazonaws.com") || - normalizedIssuer.contains("cn=amazon") || - normalizedIssuer.contains("o=amazon.com inc.") || - normalizedIssuer.contains("o=amazon web services") || - normalizedIssuer.contains("o=amazon.com, inc.") || - normalizedIssuer.contains("cn=amazon web services") || - // Support for different AWS partitions - normalizedIssuer.contains("amazonaws.com") && normalizedIssuer.contains("amazon"); - } - - /** - * Validates that the certificate subject is appropriate for Amazon SNS. - *

- * This method checks the certificate subject DN to ensure it contains - * identifiers that are consistent with Amazon SNS certificates. - * - * @param subjectDN The subject DN to validate. - * @return true if the subject is valid for SNS, false otherwise. - */ - private static boolean isValidSnsSubject(String subjectDN) { - if (subjectDN == null) { - return false; - } - - // Convert to lowercase for case-insensitive matching - String normalizedSubject = subjectDN.toLowerCase(); - - // Check for SNS-related subject patterns - return normalizedSubject.contains("sns.amazonaws.com") || - normalizedSubject.contains("amazon") || - normalizedSubject.contains("aws") || - // Allow certificates that contain amazonaws.com domain - normalizedSubject.contains("amazonaws.com"); - } - - private static String getSignatureAlgorithm(String signatureVersion) { - switch (signatureVersion) { - case SIGNATURE_VERSION_1: - return SHA1_WITH_RSA; - case SIGNATURE_VERSION_2: - return SHA256_WITH_RSA; - default: - throw SnsSignatureValidationException.builder() - .message("Unsupported signature version: " + signatureVersion + - ". Supported versions are: " + SIGNATURE_VERSION_1 + ", " + SIGNATURE_VERSION_2) - .build(); - } - } - - private static String buildCanonicalMessage(SnsMessage message) { - StringBuilder canonical = new StringBuilder(); - - // Build canonical string according to SNS specification - // The order and format must match exactly what SNS uses for signing - - canonical.append("Message\n"); - canonical.append(message.message()).append("\n"); - - canonical.append("MessageId\n"); - canonical.append(message.messageId()).append("\n"); - - // Subject is optional but must be included if present - if (message.subject().isPresent()) { - canonical.append("Subject\n"); - canonical.append(message.subject().get()).append("\n"); - } - - canonical.append("Timestamp\n"); - canonical.append(message.timestamp().toString()).append("\n"); - - canonical.append("TopicArn\n"); - canonical.append(message.topicArn()).append("\n"); - - canonical.append("Type\n"); - canonical.append(message.type()).append("\n"); - - return canonical.toString(); - } - - private static void verifySignature(String signatureBase64, String canonicalMessage, - PublicKey publicKey, String signatureAlgorithm) { - try { - // Decode the base64 signature - byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64); - - // Initialize signature verification - Signature signature = Signature.getInstance(signatureAlgorithm); - signature.initVerify(publicKey); - signature.update(canonicalMessage.getBytes(StandardCharsets.UTF_8)); - - // Verify the signature - boolean isValid = signature.verify(signatureBytes); - - if (!isValid) { - throw SnsSignatureValidationException.builder() - .message("Message signature verification failed. The message may have been tampered with or " + - "is not from Amazon SNS.") - .build(); - } - - } catch (IllegalArgumentException e) { - throw SnsSignatureValidationException.builder() - .message("Invalid base64 signature format: " + e.getMessage()) - .cause(e) - .build(); - } catch (NoSuchAlgorithmException e) { - throw SnsSignatureValidationException.builder() - .message("Signature algorithm not supported: " + signatureAlgorithm) - .cause(e) - .build(); - } catch (InvalidKeyException e) { - throw SnsSignatureValidationException.builder() - .message("Invalid public key for signature verification: " + e.getMessage()) - .cause(e) - .build(); - } catch (SignatureException e) { - throw SnsSignatureValidationException.builder() - .message("Signature verification failed: " + e.getMessage()) - .cause(e) - .build(); - } - } -} \ No newline at end of file diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/SnsMessageParser.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/SnsMessageParser.java deleted file mode 100644 index bfd522f9cc8e..000000000000 --- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/internal/messagemanager/SnsMessageParser.java +++ /dev/null @@ -1,505 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.services.sns.internal.messagemanager; - -import java.time.Instant; -import java.time.format.DateTimeParseException; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.protocols.jsoncore.JsonNode; -import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; -import software.amazon.awssdk.services.sns.messagemanager.SnsMessage; -import software.amazon.awssdk.services.sns.messagemanager.SnsMessageParsingException; -import software.amazon.awssdk.utils.StringUtils; -import software.amazon.awssdk.utils.Validate; - -/** - * Internal parser for SNS message JSON payloads. - */ -@SdkInternalApi -public final class SnsMessageParser { - - private static final JsonNodeParser JSON_PARSER = JsonNodeParser.create(); - - // Supported message types - private static final String TYPE_NOTIFICATION = "Notification"; - private static final String TYPE_SUBSCRIPTION_CONFIRMATION = "SubscriptionConfirmation"; - private static final String TYPE_UNSUBSCRIBE_CONFIRMATION = "UnsubscribeConfirmation"; - - // Required fields for all message types - private static final Set COMMON_REQUIRED_FIELDS = createSet( - "Type", "MessageId", "TopicArn", "Timestamp", "SignatureVersion", "Signature", "SigningCertURL" - ); - - // Required fields specific to Notification messages - private static final Set NOTIFICATION_REQUIRED_FIELDS = createSet("Message"); - - // Required fields specific to confirmation messages - private static final Set CONFIRMATION_REQUIRED_FIELDS = createSet("Message", "Token"); - - // All valid fields that can appear in SNS messages - private static final Set VALID_FIELDS = createSet( - "Type", "MessageId", "TopicArn", "Subject", "Message", "Timestamp", - "SignatureVersion", "Signature", "SigningCertURL", "UnsubscribeURL", "Token", "MessageAttributes" - ); - - private SnsMessageParser() { - // Utility class - prevent instantiation - } - - private static Set createSet(String... elements) { - Set set = new HashSet<>(); - for (String element : elements) { - set.add(element); - } - return Collections.unmodifiableSet(set); - } - - /** - * Parses an SNS message from JSON string with comprehensive validation and error reporting. - * - * @param messageJson The JSON string to parse. - * @return The parsed SNS message. - * @throws SnsMessageParsingException If parsing or validation fails. - */ - public static SnsMessage parseMessage(String messageJson) { - // Enhanced input validation - validateMessageJsonInput(messageJson); - - try { - JsonNode rootNode = JSON_PARSER.parse(messageJson); - return parseMessageFromJsonNode(rootNode); - } catch (SnsMessageParsingException e) { - // Re-throw SNS parsing exceptions as-is - throw e; - } catch (Exception e) { - // Provide more specific error messages for JSON parsing failures - String errorMessage = "Failed to parse JSON message"; - if (e.getMessage() != null) { - if (e.getMessage().contains("Unexpected character")) { - errorMessage += ". The message contains invalid JSON syntax. " + - "Please ensure the message is properly formatted JSON from Amazon SNS."; - } else if (e.getMessage().contains("Unexpected end-of-input")) { - errorMessage += ". The JSON message appears to be truncated or incomplete. " + - "Please ensure the complete message was received."; - } else { - errorMessage += ". " + e.getMessage(); - } - } - - throw SnsMessageParsingException.builder() - .message(errorMessage + " Raw error: " + e.getMessage()) - .cause(e) - .build(); - } - } - - /** - * Validates the input JSON string with comprehensive error reporting. - * - * @param messageJson The JSON string to validate. - * @throws SnsMessageParsingException If validation fails. - */ - private static void validateMessageJsonInput(String messageJson) { - Validate.paramNotNull(messageJson, "messageJson"); - - if (StringUtils.isBlank(messageJson)) { - throw SnsMessageParsingException.builder() - .message("Message JSON cannot be empty or blank. Please provide a valid SNS message JSON string.") - .build(); - } - - // Check for reasonable size limits - if (messageJson.length() > 256 * 1024) { // 256KB - throw SnsMessageParsingException.builder() - .message("Message JSON is too large (" + messageJson.length() + " characters). " + - "SNS messages should typically be under 256KB.") - .build(); - } - - // Basic JSON format validation - String trimmed = messageJson.trim(); - if (!trimmed.startsWith("{")) { - throw SnsMessageParsingException.builder() - .message("Message JSON must start with '{'. Received content starts with: " + - getMessagePreview(trimmed)) - .build(); - } - - if (!trimmed.endsWith("}")) { - throw SnsMessageParsingException.builder() - .message("Message JSON must end with '}'. Received content ends with: " + - getMessageSuffix(trimmed)) - .build(); - } - - // Check for common JSON issues - if (hasUnbalancedBraces(trimmed)) { - throw SnsMessageParsingException.builder() - .message("Message JSON appears to have unbalanced braces. Please ensure the JSON is properly formatted.") - .build(); - } - } - - /** - * Gets a preview of the message content for error reporting. - */ - private static String getMessagePreview(String content) { - if (content.length() <= 50) { - return "'" + content + "'"; - } - return "'" + content.substring(0, 50) + "...'"; - } - - /** - * Gets the suffix of the message content for error reporting. - */ - private static String getMessageSuffix(String content) { - if (content.length() <= 50) { - return "'" + content + "'"; - } - return "'..." + content.substring(content.length() - 50) + "'"; - } - - /** - * Performs a basic check for unbalanced braces. - */ - private static boolean hasUnbalancedBraces(String content) { - int braceCount = 0; - for (char c : content.toCharArray()) { - if (c == '{') { - braceCount++; - } else if (c == '}') { - braceCount--; - if (braceCount < 0) { - return true; // More closing braces than opening - } - } - } - return braceCount != 0; // Should be balanced - } - - private static SnsMessage parseMessageFromJsonNode(JsonNode rootNode) { - validateJsonStructure(rootNode); - - String messageType = extractRequiredStringField(rootNode, "Type"); - validateMessageType(messageType); - validateRequiredFields(rootNode, messageType); - validateNoUnexpectedFields(rootNode); - - SnsMessage.Builder messageBuilder = SnsMessage.builder() - .type(messageType) - .messageId(extractRequiredStringField(rootNode, "MessageId")) - .topicArn(extractRequiredStringField(rootNode, "TopicArn")) - .message(extractRequiredStringField(rootNode, "Message")) - .timestamp(parseTimestamp(extractRequiredStringField(rootNode, "Timestamp"))) - .signatureVersion(extractRequiredStringField(rootNode, "SignatureVersion")) - .signature(extractRequiredStringField(rootNode, "Signature")) - .signingCertUrl(extractRequiredStringField(rootNode, "SigningCertURL")); - - // Optional fields - if (rootNode.field("Subject").isPresent()) { - messageBuilder.subject(extractStringField(rootNode, "Subject")); - } - - if (rootNode.field("UnsubscribeURL").isPresent()) { - messageBuilder.unsubscribeUrl(extractStringField(rootNode, "UnsubscribeURL")); - } - - if (rootNode.field("Token").isPresent()) { - messageBuilder.token(extractStringField(rootNode, "Token")); - } - - if (rootNode.field("MessageAttributes").isPresent()) { - messageBuilder.messageAttributes(parseMessageAttributes(rootNode.field("MessageAttributes").get())); - } - - return messageBuilder.build(); - } - - private static void validateJsonStructure(JsonNode rootNode) { - if (!rootNode.isObject()) { - throw SnsMessageParsingException.builder() - .message("Message must be a JSON object") - .build(); - } - - if (rootNode.asObject().isEmpty()) { - throw SnsMessageParsingException.builder() - .message("Message cannot be empty") - .build(); - } - } - - private static void validateMessageType(String messageType) { - if (!TYPE_NOTIFICATION.equals(messageType) && - !TYPE_SUBSCRIPTION_CONFIRMATION.equals(messageType) && - !TYPE_UNSUBSCRIBE_CONFIRMATION.equals(messageType)) { - throw SnsMessageParsingException.builder() - .message("Unsupported message type: " + messageType + ". Supported types are: " + - TYPE_NOTIFICATION + ", " + TYPE_SUBSCRIPTION_CONFIRMATION + ", " + TYPE_UNSUBSCRIBE_CONFIRMATION) - .build(); - } - } - - private static void validateRequiredFields(JsonNode rootNode, String messageType) { - Set missingFields = new HashSet<>(); - Map fields = rootNode.asObject(); - - for (String field : COMMON_REQUIRED_FIELDS) { - if (!fields.containsKey(field) || fields.get(field).isNull()) { - missingFields.add(field); - } - } - - // Check type-specific required fields - Set typeSpecificFields = getTypeSpecificRequiredFields(messageType); - for (String field : typeSpecificFields) { - if (!fields.containsKey(field) || fields.get(field).isNull()) { - missingFields.add(field); - } - } - - if (!missingFields.isEmpty()) { - throw SnsMessageParsingException.builder() - .message("Missing required fields for message type '" + messageType + "': " + missingFields) - .build(); - } - } - - private static Set getTypeSpecificRequiredFields(String messageType) { - switch (messageType) { - case TYPE_NOTIFICATION: - return NOTIFICATION_REQUIRED_FIELDS; - case TYPE_SUBSCRIPTION_CONFIRMATION: - case TYPE_UNSUBSCRIBE_CONFIRMATION: - return CONFIRMATION_REQUIRED_FIELDS; - default: - return Collections.emptySet(); - } - } - - private static void validateNoUnexpectedFields(JsonNode rootNode) { - Set unexpectedFields = new HashSet<>(); - Map fields = rootNode.asObject(); - - for (String fieldName : fields.keySet()) { - if (!VALID_FIELDS.contains(fieldName)) { - unexpectedFields.add(fieldName); - } - } - - if (!unexpectedFields.isEmpty()) { - throw SnsMessageParsingException.builder() - .message("Message contains unexpected fields: " + unexpectedFields + - ". Valid fields are: " + VALID_FIELDS) - .build(); - } - } - - private static String extractRequiredStringField(JsonNode rootNode, String fieldName) { - JsonNode fieldNode = rootNode.field(fieldName).orElse(null); - if (fieldNode == null || fieldNode.isNull()) { - throw SnsMessageParsingException.builder() - .message("Required field '" + fieldName + "' is missing or null. " + - "This field is mandatory for all SNS messages. Please ensure the message " + - "is a valid SNS message from Amazon.") - .build(); - } - - if (!fieldNode.isString()) { - String actualType = getJsonNodeTypeName(fieldNode); - throw SnsMessageParsingException.builder() - .message("Field '" + fieldName + "' must be a string but found " + actualType + ". " + - "SNS message fields should be string values. Received value: " + - getFieldValuePreview(fieldNode)) - .build(); - } - - String value = fieldNode.asString(); - if (StringUtils.isBlank(value)) { - throw SnsMessageParsingException.builder() - .message("Required field '" + fieldName + "' cannot be empty or blank. " + - "This field must contain a valid value for SNS message processing.") - .build(); - } - - // Additional field-specific validation - validateFieldContent(fieldName, value); - - return value; - } - - private static String extractStringField(JsonNode rootNode, String fieldName) { - JsonNode fieldNode = rootNode.field(fieldName).orElse(null); - if (fieldNode == null || fieldNode.isNull()) { - return null; - } - - if (!fieldNode.isString()) { - String actualType = getJsonNodeTypeName(fieldNode); - throw SnsMessageParsingException.builder() - .message("Field '" + fieldName + "' must be a string but found " + actualType + ". " + - "Received value: " + getFieldValuePreview(fieldNode)) - .build(); - } - - String value = fieldNode.asString(); - - // Additional field-specific validation for optional fields - if (!StringUtils.isBlank(value)) { - validateFieldContent(fieldName, value); - } - - return value; - } - - /** - * Gets a human-readable name for the JSON node type. - */ - private static String getJsonNodeTypeName(JsonNode node) { - if (node.isNumber()) { - return "number"; - } else if (node.isBoolean()) { - return "boolean"; - } else if (node.isArray()) { - return "array"; - } else if (node.isObject()) { - return "object"; - } else { - return "unknown type"; - } - } - - /** - * Gets a preview of the field value for error reporting. - */ - private static String getFieldValuePreview(JsonNode node) { - String value = node.toString(); - if (value.length() > 100) { - return value.substring(0, 100) + "..."; - } - return value; - } - - /** - * Validates field content based on field-specific rules. - */ - private static void validateFieldContent(String fieldName, String value) { - switch (fieldName) { - case "Type": - // Already validated in validateMessageType - break; - case "MessageId": - if (value.length() > 100) { - throw SnsMessageParsingException.builder() - .message("MessageId is too long (" + value.length() + " characters). " + - "SNS MessageIds should be reasonable length identifiers.") - .build(); - } - break; - case "TopicArn": - if (!value.startsWith("arn:")) { - throw SnsMessageParsingException.builder() - .message("TopicArn must be a valid ARN starting with 'arn:'. " + - "Received: " + (value.length() > 50 ? value.substring(0, 50) + "..." : value)) - .build(); - } - if (!value.contains(":sns:")) { - throw SnsMessageParsingException.builder() - .message("TopicArn must be an SNS topic ARN containing ':sns:'. " + - "Received: " + (value.length() > 50 ? value.substring(0, 50) + "..." : value)) - .build(); - } - break; - case "SigningCertURL": - if (!value.startsWith("https://")) { - throw SnsMessageParsingException.builder() - .message("SigningCertURL must use HTTPS protocol for security. " + - "Received URL: " + (value.length() > 100 ? value.substring(0, 100) + "..." : value)) - .build(); - } - break; - case "UnsubscribeURL": - if (!value.startsWith("https://")) { - throw SnsMessageParsingException.builder() - .message("UnsubscribeURL must use HTTPS protocol for security. " + - "Received URL: " + (value.length() > 100 ? value.substring(0, 100) + "..." : value)) - .build(); - } - break; - case "SignatureVersion": - if (!"1".equals(value) && !"2".equals(value)) { - throw SnsMessageParsingException.builder() - .message("SignatureVersion must be '1' or '2'. Received: '" + value + "'") - .build(); - } - break; - default: - // No specific validation for other fields - break; - } - } - - private static Instant parseTimestamp(String timestampStr) { - try { - return Instant.parse(timestampStr); - } catch (DateTimeParseException e) { - throw SnsMessageParsingException.builder() - .message("Invalid timestamp format: " + timestampStr + ". Expected ISO-8601 format.") - .cause(e) - .build(); - } - } - - private static Map parseMessageAttributes(JsonNode messageAttributesNode) { - if (messageAttributesNode.isNull()) { - return Collections.emptyMap(); - } - - if (!messageAttributesNode.isObject()) { - throw SnsMessageParsingException.builder() - .message("MessageAttributes must be a JSON object") - .build(); - } - - Map attributes = new HashMap<>(); - Map fields = messageAttributesNode.asObject(); - - for (Map.Entry entry : fields.entrySet()) { - String key = entry.getKey(); - JsonNode valueNode = entry.getValue(); - - if (valueNode.isNull()) { - continue; // Skip null values - } - - if (!valueNode.isString()) { - throw SnsMessageParsingException.builder() - .message("MessageAttribute value for key '" + key + "' must be a string") - .build(); - } - - attributes.put(key, valueNode.asString()); - } - - return attributes; - } -} \ No newline at end of file diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/MessageManagerConfiguration.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/MessageManagerConfiguration.java deleted file mode 100644 index 5550f3944450..000000000000 --- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/MessageManagerConfiguration.java +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.services.sns.messagemanager; - -import java.time.Duration; -import java.util.Objects; -import software.amazon.awssdk.annotations.Immutable; -import software.amazon.awssdk.annotations.NotThreadSafe; -import software.amazon.awssdk.annotations.SdkPublicApi; -import software.amazon.awssdk.annotations.ThreadSafe; -import software.amazon.awssdk.http.SdkHttpClient; -import software.amazon.awssdk.utils.ToString; -import software.amazon.awssdk.utils.Validate; -import software.amazon.awssdk.utils.builder.CopyableBuilder; -import software.amazon.awssdk.utils.builder.ToCopyableBuilder; - -/** - * Configuration for the SNS Message Manager. - *

- * This class allows customization of certificate caching behavior, HTTP client settings, - * and other validation parameters for the SNS message validation process. - *

- * Example usage: - *

- * {@code
- * MessageManagerConfiguration config = MessageManagerConfiguration.builder()
- *     .certificateCacheTimeout(Duration.ofHours(1))
- *     .build();
- * 
- * SnsMessageManager manager = SnsMessageManager.builder()
- *     .configuration(config)
- *     .build();
- * }
- * 
- */ -@SdkPublicApi -@Immutable -@ThreadSafe -public final class MessageManagerConfiguration - implements ToCopyableBuilder { - - private static final Duration DEFAULT_CERTIFICATE_CACHE_TIMEOUT = Duration.ofMinutes(5); - - private final Duration certificateCacheTimeout; - private final SdkHttpClient httpClient; - - private MessageManagerConfiguration(DefaultBuilder builder) { - this.certificateCacheTimeout = builder.certificateCacheTimeout != null - ? builder.certificateCacheTimeout - : DEFAULT_CERTIFICATE_CACHE_TIMEOUT; - this.httpClient = builder.httpClient; - } - - /** - * Creates a new builder for {@link MessageManagerConfiguration}. - * - * @return A new builder instance. - */ - public static Builder builder() { - return new DefaultBuilder(); - } - - /** - * Returns the certificate cache timeout duration. - *

- * This determines how long certificates are cached before being re-fetched from AWS. - * A longer timeout reduces HTTP requests but may delay detection of certificate changes. - * - * @return The certificate cache timeout (never null). - */ - public Duration certificateCacheTimeout() { - return certificateCacheTimeout; - } - - /** - * Returns the HTTP client to use for certificate retrieval. - *

- * If not specified, the default SDK HTTP client will be used. - * - * @return The HTTP client, or null if the default should be used. - */ - public SdkHttpClient httpClient() { - return httpClient; - } - - @Override - public Builder toBuilder() { - return new DefaultBuilder(this); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - MessageManagerConfiguration that = (MessageManagerConfiguration) obj; - return Objects.equals(certificateCacheTimeout, that.certificateCacheTimeout) && - Objects.equals(httpClient, that.httpClient); - } - - @Override - public int hashCode() { - return Objects.hashCode(certificateCacheTimeout) * 31 + Objects.hashCode(httpClient); - } - - @Override - public String toString() { - return ToString.builder("MessageManagerConfiguration") - .add("certificateCacheTimeout", certificateCacheTimeout) - .add("httpClient", httpClient) - .build(); - } - - /** - * Builder for creating {@link MessageManagerConfiguration} instances. - */ - @NotThreadSafe - public interface Builder extends CopyableBuilder { - - /** - * Sets the certificate cache timeout duration. - *

- * This determines how long certificates are cached before being re-fetched from AWS. - * Must be positive. - * - * @param certificateCacheTimeout The cache timeout duration. - * @return This builder for method chaining. - * @throws IllegalArgumentException If the timeout is null or not positive. - */ - Builder certificateCacheTimeout(Duration certificateCacheTimeout); - - /** - * Sets the HTTP client to use for certificate retrieval. - *

- * If not specified, the default SDK HTTP client will be used. - * - * @param httpClient The HTTP client to use. - * @return This builder for method chaining. - */ - Builder httpClient(SdkHttpClient httpClient); - - /** - * Applies a mutation to this builder using the provided consumer. - *

- * This is a convenience method that allows for fluent configuration using lambda expressions. - * - * @param mutator A consumer that applies mutations to this builder. - * @return This builder for method chaining. - */ - default Builder applyMutation(java.util.function.Consumer mutator) { - mutator.accept(this); - return this; - } - } - - private static final class DefaultBuilder implements Builder { - private Duration certificateCacheTimeout; - private SdkHttpClient httpClient; - - private DefaultBuilder() { - } - - private DefaultBuilder(MessageManagerConfiguration configuration) { - this.certificateCacheTimeout = configuration.certificateCacheTimeout; - this.httpClient = configuration.httpClient; - } - - @Override - public Builder certificateCacheTimeout(Duration certificateCacheTimeout) { - validateCertificateCacheTimeout(certificateCacheTimeout); - this.certificateCacheTimeout = certificateCacheTimeout; - return this; - } - - @Override - public Builder httpClient(SdkHttpClient httpClient) { - // HTTP client can be null (will use default), but if provided should be valid - if (httpClient != null) { - validateHttpClient(httpClient); - } - this.httpClient = httpClient; - return this; - } - - /** - * Validates the certificate cache timeout parameter. - */ - private void validateCertificateCacheTimeout(Duration certificateCacheTimeout) { - Validate.paramNotNull(certificateCacheTimeout, "certificateCacheTimeout"); - - if (certificateCacheTimeout.isNegative() || certificateCacheTimeout.isZero()) { - throw new IllegalArgumentException( - "Certificate cache timeout must be positive. Received: " + certificateCacheTimeout + - ". Recommended values are between 1 minute and 24 hours."); - } - - // Warn about potentially problematic values - long seconds = certificateCacheTimeout.getSeconds(); - if (seconds < 30) { - // Very short cache timeout - might cause excessive HTTP requests - // Note: In a real implementation, this might use a logger instead of throwing - throw new IllegalArgumentException( - "Certificate cache timeout is very short (" + certificateCacheTimeout + - "). This may cause excessive HTTP requests to certificate servers. " + - "Consider using a timeout of at least 30 seconds."); - } - - long days = seconds / (24 * 60 * 60); // Convert seconds to days - if (days > 7) { - // Very long cache timeout - might delay certificate updates - throw new IllegalArgumentException( - "Certificate cache timeout is very long (" + certificateCacheTimeout + - "). This may delay detection of certificate changes or revocations. " + - "Consider using a timeout of 7 days or less."); - } - } - - /** - * Validates the HTTP client parameter. - */ - private void validateHttpClient(SdkHttpClient httpClient) { - // Basic validation - ensure the client is not in a closed state - // Note: There's no standard way to check if an SdkHttpClient is closed, - // so we do basic validation here - try { - // The client should be able to provide basic information - // This is a minimal check - in practice, the client will be validated - // when actually used for HTTP requests - if (httpClient.toString() == null) { - throw new IllegalArgumentException("HTTP client appears to be invalid or corrupted"); - } - } catch (Exception e) { - throw new IllegalArgumentException( - "HTTP client validation failed: " + e.getMessage() + - ". Please ensure the HTTP client is properly configured and not closed.", e); - } - } - - @Override - public MessageManagerConfiguration build() { - return new MessageManagerConfiguration(this); - } - } -} \ No newline at end of file diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsCertificateException.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsCertificateException.java deleted file mode 100644 index 259f8c1c29f8..000000000000 --- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsCertificateException.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.services.sns.messagemanager; - -import software.amazon.awssdk.annotations.SdkPublicApi; - -/** - * Exception thrown when certificate retrieval or validation fails during SNS message verification. - *

- * This exception is thrown when there are issues with the certificates used to verify SNS message signatures. - * Certificate validation is a critical security step that ensures messages are genuinely from Amazon SNS. - *

- * Common scenarios that trigger this exception: - *

    - *
  • Certificate URL is not from a trusted SNS-signed domain
  • - *
  • Certificate retrieval fails (network issues, invalid URL, etc.)
  • - *
  • Certificate chain of trust validation fails
  • - *
  • Certificate is not issued by Amazon SNS
  • - *
  • Certificate has expired or is not yet valid
  • - *
  • Certificate format is invalid or corrupted
  • - *
- *

- * When this exception is thrown, the message should be considered untrusted and should not be processed, - * as the certificate validation is essential for ensuring message authenticity. - */ -@SdkPublicApi -public class SnsCertificateException extends SnsMessageValidationException { - - private static final long serialVersionUID = 1L; - - /** - * Constructs a new SnsCertificateException with the specified detail message. - * - * @param message The detail message explaining the certificate validation failure. - */ - public SnsCertificateException(String message) { - super(message); - } - - /** - * Constructs a new SnsCertificateException with the specified detail message and cause. - * - * @param message The detail message explaining the certificate validation failure. - * @param cause The underlying cause of the certificate validation failure. - */ - public SnsCertificateException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Creates a new builder for constructing SnsCertificateException instances. - * - * @return A new builder instance. - */ - public static SnsMessageValidationException.Builder builder() { - return new SnsMessageValidationException.Builder() { - @Override - public SnsMessageValidationException build() { - if (cause != null) { - return new SnsCertificateException(message, cause); - } - return new SnsCertificateException(message); - } - }; - } -} \ No newline at end of file diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessage.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessage.java deleted file mode 100644 index 46d113027b1b..000000000000 --- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessage.java +++ /dev/null @@ -1,459 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.services.sns.messagemanager; - -import java.time.Instant; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import software.amazon.awssdk.annotations.SdkPublicApi; -import software.amazon.awssdk.utils.ToString; -import software.amazon.awssdk.utils.Validate; - -/** - * Represents a validated SNS message with all its attributes. - * - *

This class provides access to all standard SNS message fields after successful signature validation. - * The message has been cryptographically verified to be authentic and from Amazon SNS. - * - *

Supports all SNS message types: - *

    - *
  • Notification: Standard SNS notifications
  • - *
  • SubscriptionConfirmation: Subscription confirmation messages
  • - *
  • UnsubscribeConfirmation: Unsubscribe confirmation messages
  • - *
- * - *

This class is immutable and thread-safe. All required fields are validated during construction. - * Instances are typically created through the {@link SnsMessageManager#parseMessage(String)} method - * after successful message validation. - * - *

Example usage: - *

{@code
- * SnsMessageManager messageManager = SnsMessageManager.builder().build();
- * SnsMessage message = messageManager.parseMessage(jsonMessageBody);
- * 
- * // Access message properties
- * String messageType = message.type();
- * String content = message.message();
- * String topicArn = message.topicArn();
- * 
- * // Handle optional fields
- * message.subject().ifPresent(subject -> 
- *     System.out.println("Subject: " + subject));
- * }
- * - * @see SnsMessageManager - */ -@SdkPublicApi -public final class SnsMessage { - - private final String type; - private final String messageId; - private final String topicArn; - private final String subject; - private final String message; - private final Instant timestamp; - private final String signatureVersion; - private final String signature; - private final String signingCertUrl; - private final String unsubscribeUrl; - private final String token; - private final Map messageAttributes; - - private SnsMessage(Builder builder) { - this.type = Validate.paramNotNull(builder.type, "type"); - this.messageId = Validate.paramNotNull(builder.messageId, "messageId"); - this.topicArn = Validate.paramNotNull(builder.topicArn, "topicArn"); - this.subject = builder.subject; - this.message = Validate.paramNotNull(builder.message, "message"); - this.timestamp = Validate.paramNotNull(builder.timestamp, "timestamp"); - this.signatureVersion = Validate.paramNotNull(builder.signatureVersion, "signatureVersion"); - this.signature = Validate.paramNotNull(builder.signature, "signature"); - this.signingCertUrl = Validate.paramNotNull(builder.signingCertUrl, "signingCertUrl"); - this.unsubscribeUrl = builder.unsubscribeUrl; - this.token = builder.token; - this.messageAttributes = builder.messageAttributes != null - ? Collections.unmodifiableMap(builder.messageAttributes) - : Collections.emptyMap(); - } - - /** - * Creates a new builder for constructing SnsMessage instances. - * - * @return A new builder instance. - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Returns the message type. - *

- * Valid values are: - *

    - *
  • "Notification" - Standard SNS notification
  • - *
  • "SubscriptionConfirmation" - Subscription confirmation message
  • - *
  • "UnsubscribeConfirmation" - Unsubscribe confirmation message
  • - *
- * - * @return The message type (never null). - */ - public String type() { - return type; - } - - /** - * Returns the unique message identifier. - * - * @return The message ID (never null). - */ - public String messageId() { - return messageId; - } - - /** - * Returns the Amazon Resource Name (ARN) of the topic from which the message was published. - * - * @return The topic ARN (never null). - */ - public String topicArn() { - return topicArn; - } - - /** - * Returns the subject of the message, if provided. - * - *

This field is optional and may not be present in all message types. - * It is commonly used in Notification messages to provide a brief description - * of the message content. - * - * @return An Optional containing the subject, or empty if not present - */ - public Optional subject() { - return Optional.ofNullable(subject); - } - - /** - * Returns the message content. - *

- * For Notification messages, this contains the actual notification content. - * For confirmation messages, this may contain confirmation details. - * - * @return The message content (never null). - */ - public String message() { - return message; - } - - /** - * Returns the timestamp when the message was published. - * - * @return The message timestamp (never null). - */ - public Instant timestamp() { - return timestamp; - } - - /** - * Returns the signature version used to sign the message. - *

- * Valid values are: - *

    - *
  • "1" - SignatureVersion1 (SHA1)
  • - *
  • "2" - SignatureVersion2 (SHA256)
  • - *
- * - * @return The signature version (never null). - */ - public String signatureVersion() { - return signatureVersion; - } - - /** - * Returns the cryptographic signature of the message. - * - * @return The message signature (never null). - */ - public String signature() { - return signature; - } - - /** - * Returns the URL of the certificate used to sign the message. - *

- * This URL has been validated to ensure it comes from a trusted SNS-signed domain. - * - * @return The signing certificate URL (never null). - */ - public String signingCertUrl() { - return signingCertUrl; - } - - /** - * Returns the unsubscribe URL, if present. - *

- * This field is typically present in Notification messages and allows recipients - * to unsubscribe from the topic. - * - * @return An Optional containing the unsubscribe URL, or empty if not present. - */ - public Optional unsubscribeUrl() { - return Optional.ofNullable(unsubscribeUrl); - } - - /** - * Returns the token for subscription or unsubscribe confirmation, if present. - *

- * This field is required for SubscriptionConfirmation and UnsubscribeConfirmation messages. - * - * @return An Optional containing the token, or empty if not present. - */ - public Optional token() { - return Optional.ofNullable(token); - } - - /** - * Returns the message attributes, if any. - *

- * Message attributes are key-value pairs that provide additional metadata about the message. - * - * @return A map of message attributes (never null, but may be empty). - */ - public Map messageAttributes() { - return messageAttributes; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - SnsMessage that = (SnsMessage) obj; - return Objects.equals(type, that.type) && - Objects.equals(messageId, that.messageId) && - Objects.equals(topicArn, that.topicArn) && - Objects.equals(subject, that.subject) && - Objects.equals(message, that.message) && - Objects.equals(timestamp, that.timestamp) && - Objects.equals(signatureVersion, that.signatureVersion) && - Objects.equals(signature, that.signature) && - Objects.equals(signingCertUrl, that.signingCertUrl) && - Objects.equals(unsubscribeUrl, that.unsubscribeUrl) && - Objects.equals(token, that.token) && - Objects.equals(messageAttributes, that.messageAttributes); - } - - @Override - public int hashCode() { - int result = Objects.hashCode(type); - result = 31 * result + Objects.hashCode(messageId); - result = 31 * result + Objects.hashCode(topicArn); - result = 31 * result + Objects.hashCode(subject); - result = 31 * result + Objects.hashCode(message); - result = 31 * result + Objects.hashCode(timestamp); - result = 31 * result + Objects.hashCode(signatureVersion); - result = 31 * result + Objects.hashCode(signature); - result = 31 * result + Objects.hashCode(signingCertUrl); - result = 31 * result + Objects.hashCode(unsubscribeUrl); - result = 31 * result + Objects.hashCode(token); - result = 31 * result + Objects.hashCode(messageAttributes); - return result; - } - - @Override - public String toString() { - return ToString.builder("SnsMessage") - .add("type", type) - .add("messageId", messageId) - .add("topicArn", topicArn) - .add("subject", subject) - .add("timestamp", timestamp) - .add("signatureVersion", signatureVersion) - .add("hasSignature", signature != null) - .add("signingCertUrl", signingCertUrl) - .add("hasUnsubscribeUrl", unsubscribeUrl != null) - .add("hasToken", token != null) - .add("messageAttributesCount", messageAttributes.size()) - .build(); - } - - /** - * Builder for creating SnsMessage instances. - */ - public static final class Builder { - private String type; - private String messageId; - private String topicArn; - private String subject; - private String message; - private Instant timestamp; - private String signatureVersion; - private String signature; - private String signingCertUrl; - private String unsubscribeUrl; - private String token; - private Map messageAttributes; - - private Builder() { - } - - /** - * Sets the message type. - * - * @param type The message type. - * @return This builder for method chaining. - */ - public Builder type(String type) { - this.type = type; - return this; - } - - /** - * Sets the message ID. - * - * @param messageId The unique message identifier. - * @return This builder for method chaining. - */ - public Builder messageId(String messageId) { - this.messageId = messageId; - return this; - } - - /** - * Sets the topic ARN. - * - * @param topicArn The Amazon Resource Name of the topic. - * @return This builder for method chaining. - */ - public Builder topicArn(String topicArn) { - this.topicArn = topicArn; - return this; - } - - /** - * Sets the message subject. - * - * @param subject The message subject (optional). - * @return This builder for method chaining. - */ - public Builder subject(String subject) { - this.subject = subject; - return this; - } - - /** - * Sets the message content. - * - * @param message The message content. - * @return This builder for method chaining. - */ - public Builder message(String message) { - this.message = message; - return this; - } - - /** - * Sets the message timestamp. - * - * @param timestamp The timestamp when the message was published. - * @return This builder for method chaining. - */ - public Builder timestamp(Instant timestamp) { - this.timestamp = timestamp; - return this; - } - - /** - * Sets the signature version. - * - * @param signatureVersion The signature version used to sign the message. - * @return This builder for method chaining. - */ - public Builder signatureVersion(String signatureVersion) { - this.signatureVersion = signatureVersion; - return this; - } - - /** - * Sets the message signature. - * - * @param signature The cryptographic signature of the message. - * @return This builder for method chaining. - */ - public Builder signature(String signature) { - this.signature = signature; - return this; - } - - /** - * Sets the signing certificate URL. - * - * @param signingCertUrl The URL of the certificate used to sign the message. - * @return This builder for method chaining. - */ - public Builder signingCertUrl(String signingCertUrl) { - this.signingCertUrl = signingCertUrl; - return this; - } - - /** - * Sets the unsubscribe URL. - * - * @param unsubscribeUrl The unsubscribe URL (optional). - * @return This builder for method chaining. - */ - public Builder unsubscribeUrl(String unsubscribeUrl) { - this.unsubscribeUrl = unsubscribeUrl; - return this; - } - - /** - * Sets the confirmation token. - * - * @param token The token for subscription or unsubscribe confirmation (optional). - * @return This builder for method chaining. - */ - public Builder token(String token) { - this.token = token; - return this; - } - - /** - * Sets the message attributes. - * - * @param messageAttributes A map of message attributes. - * @return This builder for method chaining. - */ - public Builder messageAttributes(Map messageAttributes) { - this.messageAttributes = messageAttributes; - return this; - } - - /** - * Builds a new SnsMessage instance. - * - * @return A new SnsMessage with the configured properties. - * @throws IllegalArgumentException if any required field is null. - */ - public SnsMessage build() { - return new SnsMessage(this); - } - } -} \ No newline at end of file diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageManager.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageManager.java deleted file mode 100644 index 57a49bb663d5..000000000000 --- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageManager.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.services.sns.messagemanager; - -import java.io.InputStream; -import java.util.function.Consumer; -import software.amazon.awssdk.annotations.SdkPublicApi; -import software.amazon.awssdk.services.sns.internal.messagemanager.DefaultSnsMessageManager; -import software.amazon.awssdk.utils.SdkAutoCloseable; - - -/** - * Message manager for validating SNS message signatures. Create an instance using {@link #builder()}. - *

- * This manager provides automatic validation of SNS message signatures received via HTTP/HTTPS endpoints, - * ensuring that messages are genuinely from Amazon SNS and have not been tampered with during transmission. - * It supports both SignatureVersion1 (SHA1) and SignatureVersion2 (SHA256) as per AWS SNS standards. - *

- * The manager handles certificate retrieval, caching, and validation automatically, supporting different - * AWS regions and partitions (aws, aws-gov, aws-cn). - *

- * Example usage: - *

- * {@code
- * SnsMessageManager messageManager = SnsMessageManager.builder().build();
- * 
- * try {
- *     SnsMessage validatedMessage = messageManager.parseMessage(messageBody);
- *     String messageContent = validatedMessage.message();
- *     String topicArn = validatedMessage.topicArn();
- *     // Process the validated message
- * } catch (SnsMessageValidationException e) {
- *     // Handle validation failure
- *     logger.error("SNS message validation failed: {}", e.getMessage());
- * }
- * }
- * 
- */ -@SdkPublicApi -public interface SnsMessageManager extends SdkAutoCloseable { - - /** - * Creates a builder for configuring and creating an {@link SnsMessageManager}. - * - * @return A new builder. - */ - static Builder builder() { - return DefaultSnsMessageManager.builder(); - } - - /** - * Parses and validates an SNS message from an InputStream. - *

- * This method reads the JSON message payload, validates the signature using AWS cryptographic verification, - * and returns a parsed SNS message object with all message attributes if validation succeeds. - * - * @param messageStream The InputStream containing the JSON SNS message payload. - * @return A validated {@link SnsMessage} object containing all message fields. - * @throws SnsMessageValidationException If the message signature is invalid, the message format is malformed, - * or contains unexpected fields. - * @throws NullPointerException If messageStream is null. - */ - SnsMessage parseMessage(InputStream messageStream); - - /** - * Parses and validates an SNS message from a String. - *

- * This method parses the JSON message payload, validates the signature using AWS cryptographic verification, - * and returns a parsed SNS message object with all message attributes if validation succeeds. - * - * @param messageContent The String containing the JSON SNS message payload. - * @return A validated {@link SnsMessage} object containing all message fields. - * @throws SnsMessageValidationException If the message signature is invalid, the message format is malformed, - * or contains unexpected fields. - * @throws NullPointerException If messageContent is null. - */ - SnsMessage parseMessage(String messageContent); - - /** - * Builder for creating and configuring an {@link SnsMessageManager}. - */ - interface Builder { - - /** - * Sets the configuration for the message manager. - * - * @param configuration The configuration to use. - * @return This builder for method chaining. - */ - Builder configuration(MessageManagerConfiguration configuration); - - /** - * Sets the configuration for the message manager using a {@link Consumer} to configure the settings. - * - * @param configuration A {@link Consumer} to configure the {@link MessageManagerConfiguration}. - * @return This builder for method chaining. - */ - default Builder configuration(Consumer configuration) { - return configuration(MessageManagerConfiguration.builder().applyMutation(configuration).build()); - } - - /** - * Builds an instance of {@link SnsMessageManager} based on the supplied configurations. - * - * @return An initialized SnsMessageManager. - */ - SnsMessageManager build(); - } -} \ No newline at end of file diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageParsingException.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageParsingException.java deleted file mode 100644 index 06e9a0d03adf..000000000000 --- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageParsingException.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.services.sns.messagemanager; - -import software.amazon.awssdk.annotations.SdkPublicApi; - -/** - * Exception thrown when SNS message parsing fails due to JSON format errors or invalid message structure. - *

- * This exception is thrown in the following scenarios: - *

    - *
  • Invalid JSON format in the message payload
  • - *
  • Missing required fields (Type, MessageId, TopicArn, etc.)
  • - *
  • Unexpected fields or message structure
  • - *
  • Invalid field values or formats
  • - *
  • Unsupported message types
  • - *
- *

- * The exception message provides specific details about what parsing error occurred, - * helping developers identify and fix message format issues. - */ -@SdkPublicApi -public class SnsMessageParsingException extends SnsMessageValidationException { - - private static final long serialVersionUID = 1L; - - /** - * Constructs a new SnsMessageParsingException with the specified detail message. - * - * @param message The detail message explaining the parsing failure. - */ - public SnsMessageParsingException(String message) { - super(message); - } - - /** - * Constructs a new SnsMessageParsingException with the specified detail message and cause. - * - * @param message The detail message explaining the parsing failure. - * @param cause The underlying cause of the parsing failure (e.g., JSON parsing exception). - */ - public SnsMessageParsingException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Creates a new builder for constructing SnsMessageParsingException instances. - * - * @return A new builder instance. - */ - public static SnsMessageValidationException.Builder builder() { - return new SnsMessageValidationException.Builder() { - @Override - public SnsMessageValidationException build() { - if (cause != null) { - return new SnsMessageParsingException(message, cause); - } - return new SnsMessageParsingException(message); - } - }; - } -} \ No newline at end of file diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageValidationException.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageValidationException.java deleted file mode 100644 index bc1e83053aa3..000000000000 --- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageValidationException.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.services.sns.messagemanager; - -import software.amazon.awssdk.annotations.SdkPublicApi; - -/** - * Base exception for all SNS message validation failures. - *

- * This exception is thrown when SNS message validation fails for any reason, including: - *

    - *
  • JSON parsing or format errors
  • - *
  • Signature verification failures
  • - *
  • Certificate retrieval or validation problems
  • - *
  • Missing required fields
  • - *
  • Invalid message structure
  • - *
- *

- * Specific subclasses provide more detailed error information for different types of validation failures. - * - * @see SnsMessageParsingException - * @see SnsSignatureValidationException - * @see SnsCertificateException - */ -@SdkPublicApi -public class SnsMessageValidationException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - /** - * Constructs a new SnsMessageValidationException with the specified detail message. - * - * @param message The detail message explaining the validation failure. - */ - public SnsMessageValidationException(String message) { - super(message); - } - - /** - * Constructs a new SnsMessageValidationException with the specified detail message and cause. - * - * @param message The detail message explaining the validation failure. - * @param cause The underlying cause of the validation failure. - */ - public SnsMessageValidationException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Creates a new builder for constructing SnsMessageValidationException instances. - * - * @return A new builder instance. - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Builder for creating SnsMessageValidationException instances. - */ - public static class Builder { - protected String message; - protected Throwable cause; - - protected Builder() { - } - - /** - * Sets the detail message for the exception. - * - * @param message The detail message. - * @return This builder for method chaining. - */ - public Builder message(String message) { - this.message = message; - return this; - } - - /** - * Sets the underlying cause of the exception. - * - * @param cause The underlying cause. - * @return This builder for method chaining. - */ - public Builder cause(Throwable cause) { - this.cause = cause; - return this; - } - - /** - * Builds a new SnsMessageValidationException instance. - * - * @return A new exception with the configured properties. - */ - public SnsMessageValidationException build() { - if (cause != null) { - return new SnsMessageValidationException(message, cause); - } - return new SnsMessageValidationException(message); - } - } -} \ No newline at end of file diff --git a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsSignatureValidationException.java b/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsSignatureValidationException.java deleted file mode 100644 index 0eb5b8356e57..000000000000 --- a/services/sns/src/main/java/software/amazon/awssdk/services/sns/messagemanager/SnsSignatureValidationException.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.services.sns.messagemanager; - -import software.amazon.awssdk.annotations.SdkPublicApi; - -/** - * Exception thrown when SNS message signature verification fails. - *

- * This exception is thrown when the cryptographic signature of an SNS message cannot be verified, - * indicating that the message may not be authentic or may have been tampered with during transmission. - *

- * Common scenarios that trigger this exception: - *

    - *
  • Invalid or corrupted message signature
  • - *
  • Message content has been modified after signing
  • - *
  • Signature verification algorithm failure
  • - *
  • Mismatch between signature version and verification method
  • - *
  • Certificate and signature incompatibility
  • - *
- *

- * When this exception is thrown, the message should be considered untrusted and should not be processed. - */ -@SdkPublicApi -public class SnsSignatureValidationException extends SnsMessageValidationException { - - private static final long serialVersionUID = 1L; - - /** - * Constructs a new SnsSignatureValidationException with the specified detail message. - * - * @param message The detail message explaining the signature validation failure. - */ - public SnsSignatureValidationException(String message) { - super(message); - } - - /** - * Constructs a new SnsSignatureValidationException with the specified detail message and cause. - * - * @param message The detail message explaining the signature validation failure. - * @param cause The underlying cause of the signature validation failure. - */ - public SnsSignatureValidationException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Creates a new builder for constructing SnsSignatureValidationException instances. - * - * @return A new builder instance. - */ - public static SnsMessageValidationException.Builder builder() { - return new SnsMessageValidationException.Builder() { - @Override - public SnsMessageValidationException build() { - if (cause != null) { - return new SnsSignatureValidationException(message, cause); - } - return new SnsSignatureValidationException(message); - } - }; - } -} \ No newline at end of file diff --git a/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/CertificateRetrieverTest.java b/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/CertificateRetrieverTest.java deleted file mode 100644 index 3f26d4fbf4e1..000000000000 --- a/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/CertificateRetrieverTest.java +++ /dev/null @@ -1,828 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.services.sns.internal.messagemanager; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import software.amazon.awssdk.http.AbortableInputStream; -import software.amazon.awssdk.http.ExecutableHttpRequest; -import software.amazon.awssdk.http.HttpExecuteRequest; -import software.amazon.awssdk.http.HttpExecuteResponse; -import software.amazon.awssdk.http.SdkHttpClient; -import software.amazon.awssdk.http.SdkHttpFullResponse; -import software.amazon.awssdk.http.SdkHttpResponse; -import software.amazon.awssdk.services.sns.messagemanager.SnsCertificateException; - -/** - * Unit tests for {@link CertificateRetriever}. - * - *

This test class validates the certificate retrieval and caching functionality - * of the SNS message manager. It focuses on testing security validations, caching behavior, - * error handling, and thread-safety. - * - *

The test strategy includes: - *

    - *
  • Testing certificate URL validation against trusted SNS domains
  • - *
  • Testing HTTPS-only certificate retrieval
  • - *
  • Testing certificate caching functionality and TTL behavior
  • - *
  • Testing error handling for invalid URLs and network failures
  • - *
  • Testing thread-safety of cache implementation
  • - *
  • Testing certificate content validation and security checks
  • - *
- * - * @see CertificateRetriever - */ -class CertificateRetrieverTest { - - /** Valid certificate URL for US East 1 region used in tests. */ - private static final String VALID_CERT_URL_US_EAST_1 = "https://sns.us-east-1.amazonaws.com/cert.pem"; - - /** Valid certificate URL for EU West 1 region used in tests. */ - private static final String VALID_CERT_URL_EU_WEST_1 = "https://sns.eu-west-1.amazonaws.com/cert.pem"; - - /** Valid certificate URL for US Gov Cloud region used in tests. */ - private static final String VALID_CERT_URL_GOV_CLOUD = "https://sns.us-gov-west-1.amazonaws.com/cert.pem"; - - /** Valid certificate URL for China region used in tests. */ - private static final String VALID_CERT_URL_CHINA = "https://sns.cn-north-1.amazonaws.com.cn/cert.pem"; - - /** - * Valid PEM-encoded X.509 certificate used for testing certificate parsing and validation. - * This is a minimal test certificate that passes basic format validation. - */ - private static final String VALID_PEM_CERTIFICATE = - "-----BEGIN CERTIFICATE-----\n" + - "MIIBkTCB+wIJAKZV5i2qhHcmMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNVBAMMCWxv\n" + - "Y2FsaG9zdDAeFw0yMzAxMDEwMDAwMDBaFw0yNDAxMDEwMDAwMDBaMBQxEjAQBgNV\n" + - "BAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuVqVeII=\n" + - "-----END CERTIFICATE-----"; - - /** - * Valid DER-encoded X.509 certificate used for testing binary certificate format handling. - * This represents the same certificate as {@link #VALID_PEM_CERTIFICATE} in DER format. - */ - private static final byte[] VALID_DER_CERTIFICATE = { - (byte) 0x30, (byte) 0x82, (byte) 0x01, (byte) 0x91, (byte) 0x30, (byte) 0x82, (byte) 0x01, (byte) 0x3A, - (byte) 0x02, (byte) 0x09, (byte) 0x00, (byte) 0xA6, (byte) 0x55, (byte) 0xE6, (byte) 0x2D, (byte) 0xAA, - (byte) 0x84, (byte) 0x77, (byte) 0x26, (byte) 0x30, (byte) 0x0D, (byte) 0x06, (byte) 0x09, (byte) 0x2A, - (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01, (byte) 0x01, (byte) 0x05, - (byte) 0x05, (byte) 0x00, (byte) 0x30, (byte) 0x14, (byte) 0x31, (byte) 0x12, (byte) 0x30, (byte) 0x10, - (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x03, (byte) 0x0C, (byte) 0x09, (byte) 0x6C, - (byte) 0x6F, (byte) 0x63, (byte) 0x61, (byte) 0x6C, (byte) 0x68, (byte) 0x6F, (byte) 0x73, (byte) 0x74, - (byte) 0x30, (byte) 0x1E, (byte) 0x17, (byte) 0x0D, (byte) 0x32, (byte) 0x33, (byte) 0x30, (byte) 0x31, - (byte) 0x30, (byte) 0x31, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, - (byte) 0x5A, (byte) 0x17, (byte) 0x0D, (byte) 0x32, (byte) 0x34, (byte) 0x30, (byte) 0x31, (byte) 0x30, - (byte) 0x31, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x30, (byte) 0x5A, - (byte) 0x30, (byte) 0x14, (byte) 0x31, (byte) 0x12, (byte) 0x30, (byte) 0x10, (byte) 0x06, (byte) 0x03, - (byte) 0x55, (byte) 0x04, (byte) 0x03, (byte) 0x0C, (byte) 0x09, (byte) 0x6C, (byte) 0x6F, (byte) 0x63, - (byte) 0x61, (byte) 0x6C, (byte) 0x68, (byte) 0x6F, (byte) 0x73, (byte) 0x74, (byte) 0x30, (byte) 0x81, - (byte) 0x9F, (byte) 0x30, (byte) 0x0D, (byte) 0x06, (byte) 0x09, (byte) 0x2A, (byte) 0x86, (byte) 0x48, - (byte) 0x86, (byte) 0xF7, (byte) 0x0D, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x05, (byte) 0x00, - (byte) 0x03, (byte) 0x81, (byte) 0x8D, (byte) 0x00, (byte) 0x30, (byte) 0x81, (byte) 0x89, (byte) 0x02, - (byte) 0x81, (byte) 0x81, (byte) 0x00, (byte) 0xB9, (byte) 0x5A, (byte) 0x95, (byte) 0x78, (byte) 0x82 - }; - - /** Mock HTTP client used for testing certificate retrieval operations. */ - private SdkHttpClient mockHttpClient; - - /** Certificate retriever instance under test. */ - private CertificateRetriever certificateRetriever; - - /** - * Sets up test fixtures before each test method execution. - * - *

Initializes a mock HTTP client and creates a CertificateRetriever instance - * with a 5-minute cache timeout for testing. - */ - @BeforeEach - void setUp() { - mockHttpClient = mock(SdkHttpClient.class); - certificateRetriever = new CertificateRetriever(mockHttpClient, Duration.ofMinutes(5)); - } - - - - // ========== Constructor Validation Tests ========== - - /** - * Tests that CertificateRetriever constructor properly validates null HTTP client parameter. - * - *

This test ensures that the constructor performs proper null checking on the httpClient - * parameter and throws a {@link NullPointerException} with a descriptive error message - * when null is provided. - * - *

This validation is critical for preventing null pointer exceptions during certificate - * retrieval operations and ensuring that callers receive clear feedback about invalid parameters. - * - * @throws NullPointerException Expected exception when httpClient parameter is null - */ - @Test - void constructor_nullHttpClient_throwsException() { - assertThatThrownBy(() -> new CertificateRetriever(null, Duration.ofMinutes(5))) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("httpClient must not be null"); - } - - /** - * Tests that CertificateRetriever constructor properly validates null cache timeout parameter. - * - *

This test ensures proper null checking on the certificateCacheTimeout parameter and verifies - * that a {@link NullPointerException} is thrown with a descriptive error message when null is provided. - * - *

The cache timeout is essential for controlling certificate cache behavior and preventing - * indefinite caching of potentially compromised certificates. - * - * @throws NullPointerException Expected exception when certificateCacheTimeout parameter is null - */ - @Test - void constructor_nullCacheTimeout_throwsException() { - assertThatThrownBy(() -> new CertificateRetriever(mockHttpClient, null)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("certificateCacheTimeout must not be null"); - } - - // ========== Certificate URL Validation Tests ========== - - /** - * Tests that certificate retrieval properly validates null URL parameter. - * - *

This test ensures that the {@link CertificateRetriever#retrieveCertificate(String)} - * method performs proper null checking on the certificateUrl parameter and throws a - * {@link NullPointerException} with a descriptive error message when null is provided. - * - *

This validation is critical for preventing null pointer exceptions during URL - * processing and ensuring that callers receive clear feedback about invalid parameters. - * - * @throws NullPointerException Expected exception when certificateUrl parameter is null - */ - @Test - void retrieveCertificate_nullUrl_throwsException() { - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(null)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("certificateUrl must not be null"); - } - - /** - * Tests that certificate retrieval rejects empty URL strings. - * - *

This test verifies that empty strings are properly detected and rejected - * with an appropriate {@link SnsCertificateException}. Empty URLs cannot be - * processed for certificate retrieval. - * - * @throws SnsCertificateException Expected exception when URL is empty - */ - @Test - void retrieveCertificate_emptyUrl_throwsException() { - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate("")) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Certificate URL cannot be null or empty"); - } - - /** - * Tests that certificate retrieval rejects blank URL strings (whitespace only). - * - *

This test verifies that URLs containing only whitespace characters are - * properly detected and rejected. Such URLs are effectively empty and cannot - * be used for certificate retrieval. - * - * @throws SnsCertificateException Expected exception when URL contains only whitespace - */ - @Test - void retrieveCertificate_blankUrl_throwsException() { - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(" ")) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Certificate URL cannot be null or empty"); - } - - /** - * Tests that certificate retrieval rejects malformed URLs. - * - *

This test verifies that URLs that don't conform to valid URL syntax - * are properly detected and rejected with an appropriate error message. - * This prevents attempts to retrieve certificates from invalid locations. - * - * @throws SnsCertificateException Expected exception when URL format is invalid - */ - @Test - void retrieveCertificate_invalidUrlFormat_throwsException() { - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate("not-a-valid-url")) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Certificate URL must use HTTPS"); - } - - /** - * Tests that certificate retrieval enforces HTTPS-only policy. - * - *

This test verifies that HTTP URLs are rejected to ensure certificate - * retrieval only occurs over secure connections. This is a critical security - * requirement to prevent man-in-the-middle attacks on certificate retrieval. - * - * @throws SnsCertificateException Expected exception when URL uses HTTP instead of HTTPS - */ - @Test - void retrieveCertificate_httpUrl_throwsException() { - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate("http://sns.us-east-1.amazonaws.com/cert.pem")) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Certificate URL must use HTTPS"); - } - - /** - * Tests that certificate retrieval rejects URLs from untrusted domains. - * - *

This test verifies that only URLs from trusted SNS domains are accepted - * for certificate retrieval. This prevents attackers from providing certificates - * from malicious domains that could be used to forge SNS messages. - * - * @throws SnsCertificateException Expected exception when URL is from untrusted domain - */ - @Test - void retrieveCertificate_untrustedDomain_throwsException() { - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate("https://malicious.com/cert.pem")) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Certificate URL is not from a trusted SNS domain"); - } - - /** - * Tests that certificate retrieval accepts URLs from all trusted SNS domains. - * - *

This parameterized test verifies that certificate URLs from legitimate SNS domains - * across different AWS partitions are accepted for certificate retrieval. The test - * covers standard AWS regions, GovCloud regions, and China regions. - * - *

Trusted domains include: - *

    - *
  • Standard AWS regions: *.amazonaws.com
  • - *
  • GovCloud regions: *.amazonaws.com
  • - *
  • China regions: *.amazonaws.com.cn
  • - *
- * - * @param validUrl A valid certificate URL from a trusted SNS domain - * @throws Exception If certificate retrieval fails unexpectedly - */ - @ParameterizedTest - @ValueSource(strings = { - "https://sns.us-east-1.amazonaws.com/cert.pem", - "https://sns.eu-west-1.amazonaws.com/cert.pem", - "https://sns.ap-southeast-2.amazonaws.com/cert.pem", - "https://sns.us-gov-west-1.amazonaws.com/cert.pem", - "https://sns.us-gov-east-1.amazonaws.com/cert.pem", - "https://sns.cn-north-1.amazonaws.com.cn/cert.pem", - "https://sns.cn-northwest-1.amazonaws.com.cn/cert.pem" - }) - void retrieveCertificate_validTrustedDomains_acceptsUrl(String validUrl) throws Exception { - setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8)); - - byte[] result = certificateRetriever.retrieveCertificate(validUrl); - - assertThat(result).isNotNull(); - assertThat(new String(result, StandardCharsets.UTF_8)).contains("-----BEGIN CERTIFICATE-----"); - } - - /** - * Tests that certificate retrieval rejects URLs from various untrusted domains. - * - *

This parameterized test verifies that certificate URLs that appear similar to - * legitimate SNS domains but are actually malicious or malformed are properly rejected. - * This includes subdomain spoofing, domain spoofing, and malformed domain patterns. - * - *

The test covers various attack vectors: - *

    - *
  • Subdomain spoofing (fake-sns.us-east-1.amazonaws.com)
  • - *
  • Region spoofing (sns.fake-region.amazonaws.com)
  • - *
  • Domain spoofing (sns.us-east-1.fake.com)
  • - *
  • TLD spoofing (sns.us-east-1.amazonaws.com.fake)
  • - *
  • Malformed domains with extra dots or hyphens
  • - *
- * - * @param invalidUrl An invalid certificate URL from an untrusted domain - * @throws SnsCertificateException Expected exception when URL is from untrusted domain - */ - @ParameterizedTest - @ValueSource(strings = { - "https://fake-sns.us-east-1.amazonaws.com/cert.pem", - "https://sns.us-east-1.fake.com/cert.pem", - "https://sns.us-east-1.amazonaws.com.fake/cert.pem", - "https://malicious.amazonaws.com/cert.pem", - "https://sns..amazonaws.com/cert.pem", - "https://sns.us-east-1-.amazonaws.com/cert.pem", - "https://sns.-us-east-1.amazonaws.com/cert.pem" - }) - void retrieveCertificate_invalidTrustedDomains_throwsException(String invalidUrl) { - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(invalidUrl)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Certificate URL is not from a trusted SNS domain"); - } - - @ParameterizedTest - @ValueSource(strings = { - "https://sns.fake-region.amazonaws.com/cert.pem" - }) - void retrieveCertificate_validFormatButInvalidRegion_throwsException(String invalidUrl) { - // These URLs pass the domain pattern validation but fail during HTTP request - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(invalidUrl)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Unexpected error while retrieving certificate"); - } - - // ========== HTTP Response and Network Error Tests ========== - - /** - * Tests that certificate retrieval handles HTTP error responses appropriately. - * - *

This test verifies that when the HTTP request for certificate retrieval - * returns an error status code (such as 404 Not Found), the retriever throws - * an appropriate {@link SnsCertificateException} with details about the HTTP error. - * - *

This ensures that network-level failures are properly reported to callers - * with sufficient context for debugging and error handling. - * - * @throws SnsCertificateException Expected exception when HTTP request fails - */ - @Test - void retrieveCertificate_httpError_throwsException() throws Exception { - // Setup HTTP error response - SdkHttpResponse errorResponse = SdkHttpResponse.builder() - .statusCode(404) - .build(); - - HttpExecuteResponse httpResponse = mock(HttpExecuteResponse.class); - when(httpResponse.httpResponse()).thenReturn(errorResponse); - - ExecutableHttpRequest executableRequest = mock(ExecutableHttpRequest.class); - when(executableRequest.call()).thenReturn(httpResponse); - when(mockHttpClient.prepareRequest(any(HttpExecuteRequest.class))).thenReturn(executableRequest); - - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Unexpected error while retrieving certificate") - .hasCauseInstanceOf(SnsCertificateException.class); - } - - @Test - void retrieveCertificate_ioException_throwsException() throws Exception { - ExecutableHttpRequest executableRequest = mock(ExecutableHttpRequest.class); - when(executableRequest.call()).thenThrow(new IOException("Network error")); - when(mockHttpClient.prepareRequest(any(HttpExecuteRequest.class))).thenReturn(executableRequest); - - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("IO error while retrieving certificate") - .hasCauseInstanceOf(IOException.class); - } - - @Test - void retrieveCertificate_unexpectedException_throwsException() throws Exception { - ExecutableHttpRequest executableRequest = mock(ExecutableHttpRequest.class); - when(executableRequest.call()).thenThrow(new RuntimeException("Unexpected error")); - when(mockHttpClient.prepareRequest(any(HttpExecuteRequest.class))).thenReturn(executableRequest); - - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Unexpected error while retrieving certificate") - .hasCauseInstanceOf(RuntimeException.class); - } - - @Test - void retrieveCertificate_emptyResponseBody_throwsException() throws Exception { - SdkHttpResponse successResponse = SdkHttpResponse.builder() - .statusCode(200) - .build(); - - HttpExecuteResponse httpResponse = mock(HttpExecuteResponse.class); - when(httpResponse.httpResponse()).thenReturn(successResponse); - when(httpResponse.responseBody()).thenReturn(Optional.empty()); - - ExecutableHttpRequest executableRequest = mock(ExecutableHttpRequest.class); - when(executableRequest.call()).thenReturn(httpResponse); - when(mockHttpClient.prepareRequest(any(HttpExecuteRequest.class))).thenReturn(executableRequest); - - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Unexpected error while retrieving certificate"); - } - - @Test - void retrieveCertificate_emptyCertificate_throwsException() throws Exception { - setupSuccessfulHttpResponse(new byte[0]); - - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Unexpected error while retrieving certificate"); - } - - // ========== Certificate Content Validation Tests ========== - - /** - * Tests that certificate retrieval rejects certificates that are too small to be valid. - * - *

This test verifies that certificates smaller than the minimum expected size - * for a valid X.509 certificate are rejected. This helps prevent processing of - * malformed or truncated certificate data that could cause parsing errors. - * - *

Valid X.509 certificates, even minimal ones, should be at least 100 bytes - * due to the required ASN.1 structure and metadata. - * - * @throws SnsCertificateException Expected exception when certificate is too small - */ - @Test - void retrieveCertificate_tooSmallCertificate_throwsException() throws Exception { - byte[] tooSmallCert = "small".getBytes(StandardCharsets.UTF_8); - setupSuccessfulHttpResponse(tooSmallCert); - - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Unexpected error while retrieving certificate") - .hasCauseInstanceOf(SnsCertificateException.class); - } - - @Test - void retrieveCertificate_oversizedCertificate_throwsException() throws Exception { - // Create a certificate larger than 10KB - byte[] oversizedCert = new byte[11 * 1024]; - // Fill with valid PEM header to pass format validation - String pemHeader = "-----BEGIN CERTIFICATE-----\n"; - System.arraycopy(pemHeader.getBytes(StandardCharsets.UTF_8), 0, oversizedCert, 0, pemHeader.length()); - - setupSuccessfulHttpResponse(oversizedCert); - - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Unexpected error while retrieving certificate") - .hasCauseInstanceOf(SnsCertificateException.class); - } - - @Test - void retrieveCertificate_invalidCertificateFormat_throwsException() throws Exception { - byte[] invalidCert = "This is not a valid certificate format".getBytes(StandardCharsets.UTF_8); - // Make it large enough to pass size validation - byte[] paddedInvalidCert = new byte[200]; - System.arraycopy(invalidCert, 0, paddedInvalidCert, 0, invalidCert.length); - - setupSuccessfulHttpResponse(paddedInvalidCert); - - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Unexpected error while retrieving certificate") - .hasCauseInstanceOf(SnsCertificateException.class); - } - - @Test - void retrieveCertificate_certificateWithExcessiveNullBytes_throwsException() throws Exception { - // Create certificate with too many null bytes (over 10% of content) - byte[] certWithNulls = new byte[1000]; - String pemHeader = "-----BEGIN CERTIFICATE-----\n"; - System.arraycopy(pemHeader.getBytes(StandardCharsets.UTF_8), 0, certWithNulls, 0, pemHeader.length()); - // Fill rest with null bytes (over 10% threshold) - - setupSuccessfulHttpResponse(certWithNulls); - - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Unexpected error while retrieving certificate") - .hasCauseInstanceOf(SnsCertificateException.class); - } - - @Test - void retrieveCertificate_certificateWithConsecutiveNullBytes_throwsException() throws Exception { - // Create certificate with too many consecutive null bytes - byte[] certWithConsecutiveNulls = new byte[200]; - String pemHeader = "-----BEGIN CERTIFICATE-----\n"; - System.arraycopy(pemHeader.getBytes(StandardCharsets.UTF_8), 0, certWithConsecutiveNulls, 0, pemHeader.length()); - // Add 51 consecutive null bytes starting after the header - for (int i = pemHeader.length(); i < pemHeader.length() + 51; i++) { - certWithConsecutiveNulls[i] = 0; - } - // Fill rest with non-null data - for (int i = pemHeader.length() + 51; i < certWithConsecutiveNulls.length; i++) { - certWithConsecutiveNulls[i] = 'A'; - } - - setupSuccessfulHttpResponse(certWithConsecutiveNulls); - - assertThatThrownBy(() -> certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Unexpected error while retrieving certificate") - .hasCauseInstanceOf(SnsCertificateException.class); - } - - @Test - void retrieveCertificate_validPemCertificate_succeeds() throws Exception { - setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8)); - - byte[] result = certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1); - - assertThat(result).isNotNull(); - assertThat(new String(result, StandardCharsets.UTF_8)).isEqualTo(VALID_PEM_CERTIFICATE); - } - - @Test - void retrieveCertificate_validDerCertificate_succeeds() throws Exception { - setupSuccessfulHttpResponse(VALID_DER_CERTIFICATE); - - byte[] result = certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1); - - assertThat(result).isNotNull(); - assertThat(result).isEqualTo(VALID_DER_CERTIFICATE); - } - // ========== Certificate Caching Functionality Tests ========== - - /** - * Tests that certificate caching works correctly for repeated requests to the same URL. - * - *

This test verifies that when the same certificate URL is requested multiple times, - * the certificate is retrieved from the HTTP endpoint only once and subsequent requests - * are served from the cache. This improves performance and reduces network traffic. - * - *

The test confirms: - *

    - *
  • Both requests return identical certificate data
  • - *
  • HTTP client is called only once despite multiple requests
  • - *
  • Cache hit behavior works as expected
  • - *
- * - * @throws Exception If certificate retrieval fails unexpectedly - */ - @Test - void retrieveCertificate_cacheHit_returnsFromCache() throws Exception { - setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8)); - - // First call should fetch from HTTP - byte[] result1 = certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1); - - // Second call should return from cache without HTTP call - byte[] result2 = certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1); - - assertThat(result1).isEqualTo(result2); - // Verify HTTP client was called only once - verify(mockHttpClient, times(1)).prepareRequest(any(HttpExecuteRequest.class)); - } - - @Test - void retrieveCertificate_differentUrls_cachesIndependently() throws Exception { - setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8)); - - // Retrieve certificates from different URLs - byte[] result1 = certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1); - byte[] result2 = certificateRetriever.retrieveCertificate(VALID_CERT_URL_EU_WEST_1); - - assertThat(result1).isEqualTo(result2); // Same content but different cache entries - // Verify HTTP client was called twice (once for each URL) - verify(mockHttpClient, times(2)).prepareRequest(any(HttpExecuteRequest.class)); - - // Verify cache has both entries - assertThat(certificateRetriever.getCacheSize()).isEqualTo(2); - } - - @Test - void retrieveCertificate_expiredCache_refetchesCertificate() throws Exception { - // Create retriever with very short cache timeout - CertificateRetriever shortCacheRetriever = new CertificateRetriever(mockHttpClient, Duration.ofMillis(10)); - setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8)); - - // First call - byte[] result1 = shortCacheRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1); - - // Wait for cache to expire - Thread.sleep(20); - - // Second call should refetch - byte[] result2 = shortCacheRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1); - - assertThat(result1).isEqualTo(result2); - // Verify HTTP client was called twice due to cache expiration - verify(mockHttpClient, times(2)).prepareRequest(any(HttpExecuteRequest.class)); - } - - @Test - void clearCache_removesAllCachedCertificates() throws Exception { - setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8)); - - // Cache some certificates - certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1); - certificateRetriever.retrieveCertificate(VALID_CERT_URL_EU_WEST_1); - - assertThat(certificateRetriever.getCacheSize()).isEqualTo(2); - - // Clear cache - certificateRetriever.clearCache(); - - assertThat(certificateRetriever.getCacheSize()).isEqualTo(0); - } - - @Test - void getCacheSize_returnsCorrectSize() throws Exception { - setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8)); - - assertThat(certificateRetriever.getCacheSize()).isEqualTo(0); - - certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1); - assertThat(certificateRetriever.getCacheSize()).isEqualTo(1); - - certificateRetriever.retrieveCertificate(VALID_CERT_URL_EU_WEST_1); - assertThat(certificateRetriever.getCacheSize()).isEqualTo(2); - - // Same URL should not increase cache size - certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1); - assertThat(certificateRetriever.getCacheSize()).isEqualTo(2); - } - - // Thread-safety tests - @Test - void retrieveCertificate_concurrentAccess_threadSafe() throws Exception { - setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8)); - - int threadCount = 10; - ExecutorService executor = Executors.newFixedThreadPool(threadCount); - CountDownLatch startLatch = new CountDownLatch(1); - CountDownLatch completionLatch = new CountDownLatch(threadCount); - AtomicInteger successCount = new AtomicInteger(0); - AtomicInteger errorCount = new AtomicInteger(0); - - // Submit concurrent tasks - for (int i = 0; i < threadCount; i++) { - executor.submit(() -> { - try { - startLatch.await(); // Wait for all threads to be ready - byte[] result = certificateRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1); - if (result != null && result.length > 0) { - successCount.incrementAndGet(); - } - } catch (Exception e) { - errorCount.incrementAndGet(); - } finally { - completionLatch.countDown(); - } - }); - } - - // Start all threads simultaneously - startLatch.countDown(); - - // Wait for all threads to complete - boolean completed = completionLatch.await(5, TimeUnit.SECONDS); - - assertThat(completed).isTrue(); - assertThat(successCount.get()).isEqualTo(threadCount); - assertThat(errorCount.get()).isEqualTo(0); - - // Verify cache is thread-safe and contains only one entry - assertThat(certificateRetriever.getCacheSize()).isEqualTo(1); - - executor.shutdown(); - } - - @Test - void retrieveCertificate_concurrentDifferentUrls_threadSafe() throws Exception { - setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8)); - - int threadCount = 20; - ExecutorService executor = Executors.newFixedThreadPool(threadCount); - CountDownLatch startLatch = new CountDownLatch(1); - CountDownLatch completionLatch = new CountDownLatch(threadCount); - AtomicInteger successCount = new AtomicInteger(0); - - String[] urls = { - VALID_CERT_URL_US_EAST_1, - VALID_CERT_URL_EU_WEST_1, - VALID_CERT_URL_GOV_CLOUD, - VALID_CERT_URL_CHINA - }; - - // Submit concurrent tasks with different URLs - for (int i = 0; i < threadCount; i++) { - final String url = urls[i % urls.length]; - executor.submit(() -> { - try { - startLatch.await(); - byte[] result = certificateRetriever.retrieveCertificate(url); - if (result != null && result.length > 0) { - successCount.incrementAndGet(); - } - } catch (Exception e) { - // Ignore for this test - } finally { - completionLatch.countDown(); - } - }); - } - - startLatch.countDown(); - boolean completed = completionLatch.await(5, TimeUnit.SECONDS); - - assertThat(completed).isTrue(); - assertThat(successCount.get()).isEqualTo(threadCount); - - // Should have cached all unique URLs - assertThat(certificateRetriever.getCacheSize()).isEqualTo(urls.length); - - executor.shutdown(); - } - - @Test - void retrieveCertificate_concurrentCacheExpiration_threadSafe() throws Exception { - // Create retriever with short cache timeout for this test - CertificateRetriever shortCacheRetriever = new CertificateRetriever(mockHttpClient, Duration.ofMillis(50)); - setupSuccessfulHttpResponse(VALID_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8)); - - int threadCount = 10; - ExecutorService executor = Executors.newFixedThreadPool(threadCount); - AtomicInteger successCount = new AtomicInteger(0); - - // Submit tasks that will run over time to test cache expiration - for (int i = 0; i < threadCount; i++) { - final int delay = i * 10; // Stagger the requests - executor.submit(() -> { - try { - Thread.sleep(delay); - byte[] result = shortCacheRetriever.retrieveCertificate(VALID_CERT_URL_US_EAST_1); - if (result != null && result.length > 0) { - successCount.incrementAndGet(); - } - } catch (Exception e) { - // Ignore for this test - } - }); - } - - executor.shutdown(); - boolean terminated = executor.awaitTermination(2, TimeUnit.SECONDS); - - assertThat(terminated).isTrue(); - assertThat(successCount.get()).isEqualTo(threadCount); - } - - // ========== Test Helper Methods ========== - - /** - * Sets up a successful HTTP response mock for certificate retrieval testing. - * - *

This helper method configures the mock HTTP client to return a successful - * HTTP 200 response with the provided certificate bytes as the response body. - * This allows tests to focus on certificate processing logic without dealing - * with actual HTTP communication. - * - *

The mock setup includes: - *

    - *
  • HTTP 200 status code response
  • - *
  • Certificate bytes wrapped in an AbortableInputStream
  • - *
  • Proper mock chaining for HTTP client execution
  • - *
- * - * @param certificateBytes The certificate data to return in the HTTP response body - * @throws Exception If there are issues setting up the mock HTTP response - */ - private void setupSuccessfulHttpResponse(byte[] certificateBytes) throws Exception { - SdkHttpResponse successResponse = SdkHttpResponse.builder() - .statusCode(200) - .build(); - - HttpExecuteResponse httpResponse = mock(HttpExecuteResponse.class); - when(httpResponse.httpResponse()).thenReturn(successResponse); - // Create a new stream for each call to handle concurrent access - when(httpResponse.responseBody()).thenAnswer(invocation -> - Optional.of(AbortableInputStream.create(new ByteArrayInputStream(certificateBytes)))); - - ExecutableHttpRequest executableRequest = mock(ExecutableHttpRequest.class); - when(executableRequest.call()).thenReturn(httpResponse); - - // Make the mock thread-safe by using lenient stubbing - lenient().when(mockHttpClient.prepareRequest(any(HttpExecuteRequest.class))) - .thenReturn(executableRequest); - } -} \ No newline at end of file diff --git a/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/SignatureValidatorTest.java b/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/SignatureValidatorTest.java deleted file mode 100644 index 821ad7813aca..000000000000 --- a/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/SignatureValidatorTest.java +++ /dev/null @@ -1,375 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.services.sns.internal.messagemanager; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import software.amazon.awssdk.services.sns.messagemanager.SnsCertificateException; -import software.amazon.awssdk.services.sns.messagemanager.SnsMessage; -import software.amazon.awssdk.services.sns.messagemanager.SnsSignatureValidationException; - -/** - * Unit tests for {@link SignatureValidator}. - * - *

This test class validates the cryptographic signature verification functionality - * of the SNS message manager. It focuses on testing error conditions, input validation, - * certificate validation, and exception handling for both SHA1 and SHA256 signature algorithms. - * - *

The test strategy includes: - *

    - *
  • Testing signature verification for both SignatureVersion1 (SHA1) and SignatureVersion2 (SHA256)
  • - *
  • Testing certificate validation and chain of trust verification
  • - *
  • Testing error handling for invalid signatures and certificates
  • - *
  • Input validation tests for null parameters and malformed data
  • - *
  • Certificate parsing tests for various error conditions
  • - *
- * - *

Due to the complexity of creating valid cryptographic test data, most tests focus - * on error paths and validation logic rather than full end-to-end cryptographic verification. - * This approach effectively tests the validation components while avoiding the complexity - * of generating valid cryptographic signatures and certificates. - * - * @see SignatureValidator - * @see SnsCertificateException - * @see SnsSignatureValidationException - */ -class SignatureValidatorTest { - - // ========== Input Validation Tests ========== - - /** - * Tests that signature validation properly validates null message parameter. - * - *

This test ensures that the {@link SignatureValidator#validateSignature(SnsMessage, byte[])} - * method performs proper null checking on the message parameter and throws a - * {@link NullPointerException} with a descriptive error message when null is provided. - * - *

This validation is critical for preventing null pointer exceptions during - * signature verification and ensuring that callers receive clear feedback about - * invalid parameters. - * - * @throws NullPointerException Expected exception when message parameter is null - */ - @Test - void validateSignature_nullMessage_throwsException() { - assertThatThrownBy(() -> SignatureValidator.validateSignature(null, createInvalidCertificateBytes())) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("message must not be null"); - } - - /** - * Tests that signature validation properly validates null certificate bytes parameter. - * - *

This test ensures proper null checking on the certificateBytes parameter and verifies - * that a {@link NullPointerException} is thrown with a descriptive error message. - * - * @throws NullPointerException Expected exception when certificateBytes parameter is null - */ - @Test - void validateSignature_nullCertificateBytes_throwsException() { - SnsMessage message = createTestMessage("1"); - - assertThatThrownBy(() -> SignatureValidator.validateSignature(message, null)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("certificateBytes must not be null"); - } - - // ========== Certificate Parsing Tests ========== - - /** - * Tests that signature validation throws appropriate exception when provided with invalid certificate data. - * - *

This test verifies that the {@link SignatureValidator#validateSignature(SnsMessage, byte[])} - * method properly handles malformed certificate data by throwing a {@link SnsCertificateException} - * with an appropriate error message. - * - *

The test uses intentionally invalid certificate bytes (plain text instead of X.509 format) - * to trigger certificate parsing failure and verify proper error handling. - * - * @throws SnsCertificateException Expected exception when certificate parsing fails - */ - @Test - void validateSignature_invalidCertificateFormat_throwsException() { - SnsMessage message = createTestMessage("1"); - byte[] invalidCertificate = "invalid certificate data".getBytes(StandardCharsets.UTF_8); - - assertThatThrownBy(() -> SignatureValidator.validateSignature(message, invalidCertificate)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Failed to parse certificate"); - } - - /** - * Tests that signature validation rejects certificates that are not in valid X.509 format. - * - *

This test verifies that the validator properly handles certificate data that appears - * to be in PEM format (with BEGIN/END markers) but contains invalid certificate content. - * The validator should detect that the certificate is not a valid X.509 certificate - * and throw an appropriate exception. - * - *

This test is important for security as it ensures that malformed or spoofed - * certificates are rejected during the parsing phase, preventing potential - * security vulnerabilities. - * - * @throws SnsCertificateException Expected exception when certificate is not valid X.509 format - */ - @Test - void validateSignature_nonX509Certificate_throwsException() { - SnsMessage message = createTestMessage("1"); - byte[] nonX509Certificate = "-----BEGIN CERTIFICATE-----\nNot a real certificate\n-----END CERTIFICATE-----" - .getBytes(StandardCharsets.UTF_8); - - assertThatThrownBy(() -> SignatureValidator.validateSignature(message, nonX509Certificate)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Failed to parse certificate"); - } - - /** - * Tests certificate parsing failure with empty certificate data. - * - *

This test verifies that empty certificate bytes are properly handled - * and result in an appropriate parsing exception. - * - * @throws SnsCertificateException Expected exception when certificate data is empty - */ - @Test - void validateSignature_emptyCertificateBytes_throwsException() { - SnsMessage message = createTestMessage("1"); - byte[] emptyCertificate = new byte[0]; - - assertThatThrownBy(() -> SignatureValidator.validateSignature(message, emptyCertificate)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Failed to parse certificate"); - } - - // ========== Certificate Chain of Trust Validation Tests ========== - - /** - * Tests certificate validation with various invalid certificate formats. - * - *

Due to the complexity of creating valid cryptographic test data, these tests focus - * on certificate parsing and validation error handling rather than full cryptographic verification. - * This approach effectively tests the validation components while avoiding the complexity - * of generating valid cryptographic signatures and certificates. - * - * @throws SnsCertificateException Expected exception when certificate validation fails - */ - @Test - void validateSignature_certificateValidationFailures_throwsException() { - SnsMessage message = createTestMessage("1"); - - // Test with various invalid certificate formats that will trigger different validation failures - byte[][] invalidCertificates = { - createInvalidCertificateBytes(), - createMalformedPemCertificate(), - createEmptyCertificate() - }; - - for (byte[] invalidCert : invalidCertificates) { - assertThatThrownBy(() -> SignatureValidator.validateSignature(message, invalidCert)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Failed to parse certificate"); - } - } - - // ========== Signature Algorithm Tests ========== - - /** - * Tests signature validation with both supported signature versions. - * - *

This parameterized test verifies that both SignatureVersion1 (SHA1) and - * SignatureVersion2 (SHA256) are properly handled by the signature algorithm - * selection logic. Since we're using invalid certificates, we expect certificate - * validation to fail, but this confirms the signature version parsing works. - * - * @param signatureVersion The signature version to test ("1" for SHA1, "2" for SHA256) - * @throws SnsCertificateException Expected exception due to invalid certificate - */ - @ParameterizedTest - @ValueSource(strings = {"1", "2"}) - void validateSignature_supportedSignatureVersions_certificateValidationFails(String signatureVersion) { - SnsMessage message = createTestMessage(signatureVersion); - byte[] invalidCertificate = createInvalidCertificateBytes(); - - assertThatThrownBy(() -> SignatureValidator.validateSignature(message, invalidCertificate)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Failed to parse certificate"); - } - - /** - * Tests signature validation with an unsupported signature version. - * - *

This test verifies the behavior when an SNS message contains an unsupported - * signature version (e.g., version "3" when only versions "1" and "2" are supported). - * - *

Note: In the current implementation, certificate parsing occurs before signature - * version validation, so this test expects a certificate parsing exception rather than - * a signature version exception. This reflects the actual order of validation operations - * in the {@link SignatureValidator}. - * - * @throws SnsCertificateException Expected exception due to certificate parsing failure - * occurring before signature version validation - */ - @Test - void validateSignature_unsupportedSignatureVersion_throwsException() { - SnsMessage message = createTestMessage("3"); - byte[] invalidCertificate = createInvalidCertificateBytes(); - - assertThatThrownBy(() -> SignatureValidator.validateSignature(message, invalidCertificate)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Failed to parse certificate"); - } - - // ========== Signature Verification Tests ========== - - /** - * Tests signature verification with an invalid base64 signature. - * - *

This test uses an invalid base64 signature to verify that signature decoding - * validation works properly. Since certificate parsing occurs first, we expect - * a certificate parsing exception. - * - * @throws SnsCertificateException Expected exception due to certificate parsing failure - */ - @Test - void validateSignature_invalidBase64Signature_throwsException() { - SnsMessage message = createTestMessageWithInvalidSignature("1"); - byte[] invalidCertificate = createInvalidCertificateBytes(); - - assertThatThrownBy(() -> SignatureValidator.validateSignature(message, invalidCertificate)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Failed to parse certificate"); - } - - /** - * Tests signature verification with various signature formats. - * - *

This test verifies that different signature formats are handled appropriately. - * Since we're using invalid certificates, we expect certificate validation to fail, - * but this confirms the signature processing logic is reached. - * - * @throws SnsCertificateException Expected exception due to certificate parsing failure - */ - @Test - void validateSignature_variousSignatureFormats_throwsException() { - byte[] invalidCertificate = createInvalidCertificateBytes(); - - // Test with different signature formats - SnsMessage[] messages = { - createTestMessageWithWrongSignature("1"), - createTestMessageWithInvalidSignature("2"), - createTestMessage("1"), - createTestMessage("2") - }; - - for (SnsMessage message : messages) { - assertThatThrownBy(() -> SignatureValidator.validateSignature(message, invalidCertificate)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Failed to parse certificate"); - } - } - - // ========== Test Helper Methods ========== - - /** - * Creates invalid certificate bytes for testing certificate parsing failures. - * - * @return Invalid certificate bytes that will cause parsing to fail - */ - private byte[] createInvalidCertificateBytes() { - return "invalid certificate for testing".getBytes(StandardCharsets.UTF_8); - } - - /** - * Creates a test SNS message with the specified signature version. - * - * @param signatureVersion The signature version to use ("1", "2", etc.) - * @return A test SnsMessage with all required fields - */ - private SnsMessage createTestMessage(String signatureVersion) { - return SnsMessage.builder() - .type("Notification") - .messageId("12345678-1234-1234-1234-123456789012") - .topicArn("arn:aws:sns:us-east-1:123456789012:MyTopic") - .message("Test message content") - .timestamp(Instant.parse("2023-01-01T12:00:00.000Z")) - .signatureVersion(signatureVersion) - .signature("dGVzdCBzaWduYXR1cmU=") // "test signature" in base64 - .signingCertUrl("https://sns.us-east-1.amazonaws.com/cert.pem") - .build(); - } - - /** - * Creates a test SNS message with an invalid base64 signature. - * - * @param signatureVersion The signature version to use - * @return A test SnsMessage with an invalid signature format - */ - private SnsMessage createTestMessageWithInvalidSignature(String signatureVersion) { - return SnsMessage.builder() - .type("Notification") - .messageId("12345678-1234-1234-1234-123456789012") - .topicArn("arn:aws:sns:us-east-1:123456789012:MyTopic") - .message("Test message content") - .timestamp(Instant.parse("2023-01-01T12:00:00.000Z")) - .signatureVersion(signatureVersion) - .signature("invalid-base64-signature!@#$%") // Invalid base64 - .signingCertUrl("https://sns.us-east-1.amazonaws.com/cert.pem") - .build(); - } - - /** - * Creates a test SNS message with a valid base64 signature that doesn't match the content. - * - * @param signatureVersion The signature version to use - * @return A test SnsMessage with a wrong but valid base64 signature - */ - private SnsMessage createTestMessageWithWrongSignature(String signatureVersion) { - return SnsMessage.builder() - .type("Notification") - .messageId("12345678-1234-1234-1234-123456789012") - .topicArn("arn:aws:sns:us-east-1:123456789012:MyTopic") - .message("Test message content") - .timestamp(Instant.parse("2023-01-01T12:00:00.000Z")) - .signatureVersion(signatureVersion) - .signature("d3Jvbmcgc2lnbmF0dXJl") // "wrong signature" in base64 - .signingCertUrl("https://sns.us-east-1.amazonaws.com/cert.pem") - .build(); - } - - /** - * Creates a malformed PEM certificate for testing certificate parsing failures. - * - * @return Malformed PEM certificate bytes - */ - private byte[] createMalformedPemCertificate() { - return "-----BEGIN CERTIFICATE-----\nMalformed certificate content\n-----END CERTIFICATE-----" - .getBytes(StandardCharsets.UTF_8); - } - - /** - * Creates empty certificate bytes for testing certificate parsing failures. - * - * @return Empty certificate bytes - */ - private byte[] createEmptyCertificate() { - return new byte[0]; - } -} \ No newline at end of file diff --git a/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/SnsMessageParserTest.java b/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/SnsMessageParserTest.java deleted file mode 100644 index 8bd7b92a5581..000000000000 --- a/services/sns/src/test/java/software/amazon/awssdk/services/sns/internal/messagemanager/SnsMessageParserTest.java +++ /dev/null @@ -1,689 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.services.sns.internal.messagemanager; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; -import org.junit.jupiter.api.Test; -import software.amazon.awssdk.services.sns.messagemanager.SnsMessage; -import software.amazon.awssdk.services.sns.messagemanager.SnsMessageParsingException; - -/** - * Unit tests for {@link SnsMessageParser}. - */ -class SnsMessageParserTest { - - private static final String VALID_NOTIFICATION_JSON = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Subject\":\"Test Subject\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"," - + "\"UnsubscribeURL\":\"https://sns.us-east-1.amazonaws.com/unsubscribe\"" - + "}"; - - private static final String VALID_SUBSCRIPTION_CONFIRMATION_JSON = "{" - + "\"Type\":\"SubscriptionConfirmation\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"You have chosen to subscribe to the topic\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"2\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"," - + "\"Token\":\"confirmation-token-12345\"" - + "}"; - - private static final String VALID_UNSUBSCRIBE_CONFIRMATION_JSON = "{" - + "\"Type\":\"UnsubscribeConfirmation\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"You have been unsubscribed from the topic\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"," - + "\"Token\":\"unsubscribe-token-12345\"" - + "}"; - - @Test - void parseMessage_validNotificationMessage_parsesSuccessfully() { - SnsMessage message = SnsMessageParser.parseMessage(VALID_NOTIFICATION_JSON); - - assertThat(message.type()).isEqualTo("Notification"); - assertThat(message.messageId()).isEqualTo("12345678-1234-1234-1234-123456789012"); - assertThat(message.topicArn()).isEqualTo("arn:aws:sns:us-east-1:123456789012:MyTopic"); - assertThat(message.subject()).hasValue("Test Subject"); - assertThat(message.message()).isEqualTo("Test message content"); - assertThat(message.timestamp()).isEqualTo(Instant.parse("2023-01-01T12:00:00.000Z")); - assertThat(message.signatureVersion()).isEqualTo("1"); - assertThat(message.signature()).isEqualTo("test-signature"); - assertThat(message.signingCertUrl()).isEqualTo("https://sns.us-east-1.amazonaws.com/cert.pem"); - assertThat(message.unsubscribeUrl()).hasValue("https://sns.us-east-1.amazonaws.com/unsubscribe"); - assertThat(message.token()).isEmpty(); - assertThat(message.messageAttributes()).isEmpty(); - } - - @Test - void parseMessage_validSubscriptionConfirmationMessage_parsesSuccessfully() { - SnsMessage message = SnsMessageParser.parseMessage(VALID_SUBSCRIPTION_CONFIRMATION_JSON); - - assertThat(message.type()).isEqualTo("SubscriptionConfirmation"); - assertThat(message.messageId()).isEqualTo("12345678-1234-1234-1234-123456789012"); - assertThat(message.topicArn()).isEqualTo("arn:aws:sns:us-east-1:123456789012:MyTopic"); - assertThat(message.subject()).isEmpty(); - assertThat(message.message()).isEqualTo("You have chosen to subscribe to the topic"); - assertThat(message.timestamp()).isEqualTo(Instant.parse("2023-01-01T12:00:00.000Z")); - assertThat(message.signatureVersion()).isEqualTo("2"); - assertThat(message.signature()).isEqualTo("test-signature"); - assertThat(message.signingCertUrl()).isEqualTo("https://sns.us-east-1.amazonaws.com/cert.pem"); - assertThat(message.unsubscribeUrl()).isEmpty(); - assertThat(message.token()).hasValue("confirmation-token-12345"); - assertThat(message.messageAttributes()).isEmpty(); - } - - @Test - void parseMessage_validUnsubscribeConfirmationMessage_parsesSuccessfully() { - SnsMessage message = SnsMessageParser.parseMessage(VALID_UNSUBSCRIBE_CONFIRMATION_JSON); - - assertThat(message.type()).isEqualTo("UnsubscribeConfirmation"); - assertThat(message.messageId()).isEqualTo("12345678-1234-1234-1234-123456789012"); - assertThat(message.topicArn()).isEqualTo("arn:aws:sns:us-east-1:123456789012:MyTopic"); - assertThat(message.subject()).isEmpty(); - assertThat(message.message()).isEqualTo("You have been unsubscribed from the topic"); - assertThat(message.timestamp()).isEqualTo(Instant.parse("2023-01-01T12:00:00.000Z")); - assertThat(message.signatureVersion()).isEqualTo("1"); - assertThat(message.signature()).isEqualTo("test-signature"); - assertThat(message.signingCertUrl()).isEqualTo("https://sns.us-east-1.amazonaws.com/cert.pem"); - assertThat(message.unsubscribeUrl()).isEmpty(); - assertThat(message.token()).hasValue("unsubscribe-token-12345"); - assertThat(message.messageAttributes()).isEmpty(); - } - - @Test - void parseMessage_messageWithMessageAttributes_parsesSuccessfully() { - String jsonWithAttributes = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"," - + "\"MessageAttributes\":{" - + "\"attr1\":\"value1\"," - + "\"attr2\":\"value2\"" - + "}" - + "}"; - - SnsMessage message = SnsMessageParser.parseMessage(jsonWithAttributes); - - assertThat(message.messageAttributes()).hasSize(2); - assertThat(message.messageAttributes()).containsEntry("attr1", "value1"); - assertThat(message.messageAttributes()).containsEntry("attr2", "value2"); - } - - @Test - void parseMessage_nullInput_throwsException() { - assertThatThrownBy(() -> SnsMessageParser.parseMessage(null)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("messageJson must not be null"); - } - - @Test - void parseMessage_emptyString_throwsException() { - assertThatThrownBy(() -> SnsMessageParser.parseMessage("")) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message JSON cannot be empty or blank"); - } - - @Test - void parseMessage_blankString_throwsException() { - assertThatThrownBy(() -> SnsMessageParser.parseMessage(" ")) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message JSON cannot be empty or blank"); - } - - @Test - void parseMessage_tooLargeMessage_throwsException() { - StringBuilder largeMessage = new StringBuilder(); - for (int i = 0; i < 300000; i++) { // Over 256KB - largeMessage.append("a"); - } - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(largeMessage.toString())) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message JSON is too large"); - } - - @Test - void parseMessage_invalidJsonFormat_throwsException() { - String invalidJson = "{ invalid json }"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(invalidJson)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Failed to parse JSON message"); - } - - @Test - void parseMessage_notJsonObject_throwsException() { - String jsonArray = "[\"not\", \"an\", \"object\"]"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonArray)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message JSON must start with '{'"); - } - - @Test - void parseMessage_emptyJsonObject_throwsException() { - assertThatThrownBy(() -> SnsMessageParser.parseMessage("{}")) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message cannot be empty"); - } - - @Test - void parseMessage_missingType_throwsException() { - String jsonWithoutType = "{" - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutType)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Required field 'Type' is missing"); - } - - @Test - void parseMessage_missingMessageId_throwsException() { - String jsonWithoutMessageId = "{" - + "\"Type\":\"Notification\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutMessageId)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Missing required fields") - .hasMessageContaining("MessageId"); - } - - @Test - void parseMessage_missingTopicArn_throwsException() { - String jsonWithoutTopicArn = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutTopicArn)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Missing required fields") - .hasMessageContaining("TopicArn"); - } - - @Test - void parseMessage_missingMessage_throwsException() { - String jsonWithoutMessage = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutMessage)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Missing required fields") - .hasMessageContaining("Message"); - } - - @Test - void parseMessage_missingTimestamp_throwsException() { - String jsonWithoutTimestamp = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutTimestamp)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Missing required fields") - .hasMessageContaining("Timestamp"); - } - - @Test - void parseMessage_missingSignatureVersion_throwsException() { - String jsonWithoutSignatureVersion = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutSignatureVersion)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Missing required fields") - .hasMessageContaining("SignatureVersion"); - } - - @Test - void parseMessage_missingSignature_throwsException() { - String jsonWithoutSignature = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutSignature)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Missing required fields") - .hasMessageContaining("Signature"); - } - - @Test - void parseMessage_missingSigningCertURL_throwsException() { - String jsonWithoutSigningCertURL = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutSigningCertURL)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Missing required fields") - .hasMessageContaining("SigningCertURL"); - } - - @Test - void parseMessage_missingTokenForSubscriptionConfirmation_throwsException() { - String jsonWithoutToken = "{" - + "\"Type\":\"SubscriptionConfirmation\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"You have chosen to subscribe to the topic\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"2\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithoutToken)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Missing required fields") - .hasMessageContaining("Token"); - } - - @Test - void parseMessage_unsupportedMessageType_throwsException() { - String jsonWithUnsupportedType = "{" - + "\"Type\":\"UnsupportedType\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithUnsupportedType)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Unsupported message type: UnsupportedType") - .hasMessageContaining("Supported types are: Notification, SubscriptionConfirmation, UnsubscribeConfirmation"); - } - - @Test - void parseMessage_unexpectedFields_throwsException() { - String jsonWithUnexpectedField = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"," - + "\"UnexpectedField\":\"unexpected-value\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithUnexpectedField)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message contains unexpected fields") - .hasMessageContaining("UnexpectedField"); - } - - @Test - void parseMessage_nullFieldValue_throwsException() { - String jsonWithNullField = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":null," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithNullField)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Missing required fields") - .hasMessageContaining("MessageId"); - } - - @Test - void parseMessage_emptyFieldValue_throwsException() { - String jsonWithEmptyField = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithEmptyField)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Required field 'MessageId' cannot be empty or blank"); - } - - @Test - void parseMessage_nonStringFieldValue_throwsException() { - String jsonWithNonStringField = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":12345," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithNonStringField)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Field 'MessageId' must be a string but found number"); - } - - @Test - void parseMessage_invalidTimestampFormat_throwsException() { - String jsonWithInvalidTimestamp = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"invalid-timestamp\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithInvalidTimestamp)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Invalid timestamp format: invalid-timestamp"); - } - - @Test - void parseMessage_invalidTopicArn_throwsException() { - String jsonWithInvalidTopicArn = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"invalid-arn\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithInvalidTopicArn)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("TopicArn must be a valid ARN starting with 'arn:'"); - } - - @Test - void parseMessage_nonSnsTopicArn_throwsException() { - String jsonWithNonSnsArn = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:s3:::my-bucket\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithNonSnsArn)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("TopicArn must be an SNS topic ARN containing ':sns:'"); - } - - @Test - void parseMessage_invalidSignatureVersion_throwsException() { - String jsonWithInvalidSignatureVersion = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"3\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithInvalidSignatureVersion)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("SignatureVersion must be '1' or '2'. Received: '3'"); - } - - @Test - void parseMessage_nonHttpsSigningCertURL_throwsException() { - String jsonWithHttpCertUrl = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"http://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithHttpCertUrl)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("SigningCertURL must use HTTPS protocol for security"); - } - - @Test - void parseMessage_nonHttpsUnsubscribeURL_throwsException() { - String jsonWithHttpUnsubscribeUrl = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"," - + "\"UnsubscribeURL\":\"http://sns.us-east-1.amazonaws.com/unsubscribe\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithHttpUnsubscribeUrl)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("UnsubscribeURL must use HTTPS protocol for security"); - } - - @Test - void parseMessage_tooLongMessageId_throwsException() { - StringBuilder longMessageId = new StringBuilder(); - for (int i = 0; i < 101; i++) { // Over 100 characters - longMessageId.append("a"); - } - - String jsonWithLongMessageId = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"" + longMessageId.toString() + "\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithLongMessageId)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("MessageId is too long"); - } - - @Test - void parseMessage_invalidMessageAttributesType_throwsException() { - String jsonWithInvalidMessageAttributes = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"," - + "\"MessageAttributes\":\"not-an-object\"" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithInvalidMessageAttributes)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("MessageAttributes must be a JSON object"); - } - - @Test - void parseMessage_invalidMessageAttributeValueType_throwsException() { - String jsonWithInvalidAttributeValue = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"," - + "\"MessageAttributes\":{" - + "\"attr1\":123" - + "}" - + "}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithInvalidAttributeValue)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("MessageAttribute value for key 'attr1' must be a string"); - } - - @Test - void parseMessage_nullMessageAttributeValue_skipsAttribute() { - String jsonWithNullAttributeValue = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\"," - + "\"TopicArn\":\"arn:aws:sns:us-east-1:123456789012:MyTopic\"," - + "\"Message\":\"Test message content\"," - + "\"Timestamp\":\"2023-01-01T12:00:00.000Z\"," - + "\"SignatureVersion\":\"1\"," - + "\"Signature\":\"test-signature\"," - + "\"SigningCertURL\":\"https://sns.us-east-1.amazonaws.com/cert.pem\"," - + "\"MessageAttributes\":{" - + "\"attr1\":\"value1\"," - + "\"attr2\":null," - + "\"attr3\":\"value3\"" - + "}" - + "}"; - - SnsMessage message = SnsMessageParser.parseMessage(jsonWithNullAttributeValue); - - assertThat(message.messageAttributes()).hasSize(2); - assertThat(message.messageAttributes()).containsEntry("attr1", "value1"); - assertThat(message.messageAttributes()).containsEntry("attr3", "value3"); - assertThat(message.messageAttributes()).doesNotContainKey("attr2"); - } - - @Test - void parseMessage_unbalancedBraces_throwsException() { - String jsonWithUnbalancedBraces = "{" - + "\"Type\":\"Notification\"," - + "\"MessageId\":\"12345678-1234-1234-1234-123456789012\""; - // Missing closing brace - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(jsonWithUnbalancedBraces)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message JSON must end with '}'"); - } - - @Test - void parseMessage_doesNotStartWithBrace_throwsException() { - String invalidJson = "invalid{\"Type\":\"Notification\"}"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(invalidJson)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message JSON must start with '{'"); - } - - @Test - void parseMessage_doesNotEndWithBrace_throwsException() { - String invalidJson = "{\"Type\":\"Notification\"}invalid"; - - assertThatThrownBy(() -> SnsMessageParser.parseMessage(invalidJson)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message JSON must end with '}'"); - } -} \ No newline at end of file diff --git a/services/sns/src/test/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageManagerIntegrationTest.java b/services/sns/src/test/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageManagerIntegrationTest.java deleted file mode 100644 index a563e2d58832..000000000000 --- a/services/sns/src/test/java/software/amazon/awssdk/services/sns/messagemanager/SnsMessageManagerIntegrationTest.java +++ /dev/null @@ -1,393 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.awssdk.services.sns.messagemanager; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - * Integration tests for SnsMessageManager that verify the complete workflow - * from public API through all internal components. - */ -class SnsMessageManagerIntegrationTest { - - private SnsMessageManager messageManager; - - @BeforeEach - void setUp() { - messageManager = SnsMessageManager.builder().build(); - } - - @AfterEach - void tearDown() { - if (messageManager != null) { - messageManager.close(); - } - } - - @Test - void builder_withDefaultConfiguration_createsManagerSuccessfully() { - try (SnsMessageManager manager = SnsMessageManager.builder().build()) { - assertThat(manager).isNotNull(); - } - } - - @Test - void builder_withCustomConfiguration_createsManagerSuccessfully() { - MessageManagerConfiguration config = MessageManagerConfiguration.builder() - .certificateCacheTimeout(Duration.ofMinutes(10)) - .build(); - - try (SnsMessageManager manager = SnsMessageManager.builder() - .configuration(config) - .build()) { - assertThat(manager).isNotNull(); - } - } - - @Test - void builder_withConsumerConfiguration_createsManagerSuccessfully() { - try (SnsMessageManager manager = SnsMessageManager.builder() - .configuration(config -> config.certificateCacheTimeout(Duration.ofMinutes(15))) - .build()) { - assertThat(manager).isNotNull(); - } - } - - @Test - void parseMessage_withNullString_throwsException() { - assertThatThrownBy(() -> messageManager.parseMessage((String) null)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message content cannot be null"); - } - - @Test - void parseMessage_withNullInputStream_throwsException() { - assertThatThrownBy(() -> messageManager.parseMessage((ByteArrayInputStream) null)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message InputStream cannot be null"); - } - - @Test - void parseMessage_withEmptyString_throwsException() { - assertThatThrownBy(() -> messageManager.parseMessage("")) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message content cannot be empty"); - } - - @Test - void parseMessage_withWhitespaceOnlyString_throwsException() { - assertThatThrownBy(() -> messageManager.parseMessage(" \n\t ")) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message content cannot be empty"); - } - - @Test - void parseMessage_withInvalidJson_throwsParsingException() { - String invalidJson = "{ invalid json }"; - - assertThatThrownBy(() -> messageManager.parseMessage(invalidJson)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Failed to parse JSON message"); - } - - @Test - void parseMessage_withNonJsonString_throwsParsingException() { - String nonJson = "This is not JSON"; - - assertThatThrownBy(() -> messageManager.parseMessage(nonJson)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message content does not appear to be valid JSON"); - } - - @Test - void parseMessage_withValidJsonButMissingRequiredFields_throwsParsingException() { - String incompleteMessage = "{\"Type\": \"Notification\"}"; - - assertThatThrownBy(() -> messageManager.parseMessage(incompleteMessage)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Missing required fields"); - } - - @Test - void parseMessage_withUnsupportedMessageType_throwsParsingException() { - String messageWithInvalidType = "{" - + "\"Type\": \"InvalidType\"," - + "\"MessageId\": \"test-id\"," - + "\"TopicArn\": \"arn:aws:sns:us-east-1:123456789012:test-topic\"," - + "\"Message\": \"test message\"," - + "\"Timestamp\": \"2023-01-01T00:00:00.000Z\"," - + "\"SignatureVersion\": \"1\"," - + "\"Signature\": \"test-signature\"," - + "\"SigningCertURL\": \"https://sns.us-east-1.amazonaws.com/test.pem\"" - + "}"; - - assertThatThrownBy(() -> messageManager.parseMessage(messageWithInvalidType)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Unsupported message type: InvalidType"); - } - - @Test - void parseMessage_withInvalidCertificateUrl_throwsParsingException() { - String messageWithInvalidCertUrl = "{" - + "\"Type\": \"Notification\"," - + "\"MessageId\": \"test-id\"," - + "\"TopicArn\": \"arn:aws:sns:us-east-1:123456789012:test-topic\"," - + "\"Message\": \"test message\"," - + "\"Timestamp\": \"2023-01-01T00:00:00.000Z\"," - + "\"SignatureVersion\": \"1\"," - + "\"Signature\": \"test-signature\"," - + "\"SigningCertURL\": \"http://malicious-site.com/fake-cert.pem\"" - + "}"; - - assertThatThrownBy(() -> messageManager.parseMessage(messageWithInvalidCertUrl)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("SigningCertURL must use HTTPS protocol for security"); - } - - @Test - void parseMessage_withHttpCertificateUrl_throwsParsingException() { - String messageWithHttpCertUrl = "{" - + "\"Type\": \"Notification\"," - + "\"MessageId\": \"test-id\"," - + "\"TopicArn\": \"arn:aws:sns:us-east-1:123456789012:test-topic\"," - + "\"Message\": \"test message\"," - + "\"Timestamp\": \"2023-01-01T00:00:00.000Z\"," - + "\"SignatureVersion\": \"1\"," - + "\"Signature\": \"test-signature\"," - + "\"SigningCertURL\": \"http://sns.us-east-1.amazonaws.com/test.pem\"" - + "}"; - - assertThatThrownBy(() -> messageManager.parseMessage(messageWithHttpCertUrl)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("SigningCertURL must use HTTPS protocol for security"); - } - - @Test - void parseMessage_withValidUrlButNetworkFailure_throwsCertificateException() { - // This test uses a valid HTTPS SNS URL that will pass parsing but fail during certificate retrieval - String messageWithValidButUnreachableUrl = "{" - + "\"Type\": \"Notification\"," - + "\"MessageId\": \"test-id\"," - + "\"TopicArn\": \"arn:aws:sns:us-east-1:123456789012:test-topic\"," - + "\"Message\": \"test message\"," - + "\"Timestamp\": \"2023-01-01T00:00:00.000Z\"," - + "\"SignatureVersion\": \"1\"," - + "\"Signature\": \"test-signature\"," - + "\"SigningCertURL\": \"https://sns.us-east-1.amazonaws.com/nonexistent-cert.pem\"" - + "}"; - - // This should pass parsing but fail during certificate retrieval - assertThatThrownBy(() -> messageManager.parseMessage(messageWithValidButUnreachableUrl)) - .isInstanceOf(SnsCertificateException.class) - .hasMessageContaining("Failed to retrieve certificate"); - } - - @Test - void parseMessage_withInputStream_handlesParsingCorrectly() { - String invalidJson = "{ invalid json }"; - ByteArrayInputStream inputStream = new ByteArrayInputStream(invalidJson.getBytes(StandardCharsets.UTF_8)); - - assertThatThrownBy(() -> messageManager.parseMessage(inputStream)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Failed to parse JSON message"); - } - - @Test - void parseMessage_withEmptyInputStream_throwsException() { - ByteArrayInputStream emptyStream = new ByteArrayInputStream(new byte[0]); - - assertThatThrownBy(() -> messageManager.parseMessage(emptyStream)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("InputStream is empty"); - } - - @Test - void parseMessage_withLargeMessage_throwsException() { - // Create a message larger than 256KB - StringBuilder largeMessage = new StringBuilder("{\"Type\": \"Notification\","); - largeMessage.append("\"Message\": \""); - for (int i = 0; i < 300 * 1024; i++) { // 300KB of 'a' characters - largeMessage.append("a"); - } - largeMessage.append("\"}"); - - assertThatThrownBy(() -> messageManager.parseMessage(largeMessage.toString())) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message content is too large"); - } - - @Test - void parseMessage_withInvalidTopicArn_throwsParsingException() { - String messageWithInvalidArn = "{" - + "\"Type\": \"Notification\"," - + "\"MessageId\": \"test-id\"," - + "\"TopicArn\": \"invalid-arn\"," - + "\"Message\": \"test message\"," - + "\"Timestamp\": \"2023-01-01T00:00:00.000Z\"," - + "\"SignatureVersion\": \"1\"," - + "\"Signature\": \"test-signature\"," - + "\"SigningCertURL\": \"https://sns.us-east-1.amazonaws.com/test.pem\"" - + "}"; - - assertThatThrownBy(() -> messageManager.parseMessage(messageWithInvalidArn)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("TopicArn must be a valid ARN starting with 'arn:'"); - } - - @Test - void parseMessage_withInvalidSignatureVersion_throwsParsingException() { - String messageWithInvalidSigVersion = "{" - + "\"Type\": \"Notification\"," - + "\"MessageId\": \"test-id\"," - + "\"TopicArn\": \"arn:aws:sns:us-east-1:123456789012:test-topic\"," - + "\"Message\": \"test message\"," - + "\"Timestamp\": \"2023-01-01T00:00:00.000Z\"," - + "\"SignatureVersion\": \"3\"," - + "\"Signature\": \"test-signature\"," - + "\"SigningCertURL\": \"https://sns.us-east-1.amazonaws.com/test.pem\"" - + "}"; - - assertThatThrownBy(() -> messageManager.parseMessage(messageWithInvalidSigVersion)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("SignatureVersion must be '1' or '2'"); - } - - @Test - void parseMessage_withInvalidTimestamp_throwsParsingException() { - String messageWithInvalidTimestamp = "{" - + "\"Type\": \"Notification\"," - + "\"MessageId\": \"test-id\"," - + "\"TopicArn\": \"arn:aws:sns:us-east-1:123456789012:test-topic\"," - + "\"Message\": \"test message\"," - + "\"Timestamp\": \"invalid-timestamp\"," - + "\"SignatureVersion\": \"1\"," - + "\"Signature\": \"test-signature\"," - + "\"SigningCertURL\": \"https://sns.us-east-1.amazonaws.com/test.pem\"" - + "}"; - - assertThatThrownBy(() -> messageManager.parseMessage(messageWithInvalidTimestamp)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Invalid timestamp format"); - } - - @Test - void parseMessage_withUnexpectedFields_throwsParsingException() { - String messageWithUnexpectedField = "{" - + "\"Type\": \"Notification\"," - + "\"MessageId\": \"test-id\"," - + "\"TopicArn\": \"arn:aws:sns:us-east-1:123456789012:test-topic\"," - + "\"Message\": \"test message\"," - + "\"Timestamp\": \"2023-01-01T00:00:00.000Z\"," - + "\"SignatureVersion\": \"1\"," - + "\"Signature\": \"test-signature\"," - + "\"SigningCertURL\": \"https://sns.us-east-1.amazonaws.com/test.pem\"," - + "\"UnexpectedField\": \"should not be here\"" - + "}"; - - assertThatThrownBy(() -> messageManager.parseMessage(messageWithUnexpectedField)) - .isInstanceOf(SnsMessageParsingException.class) - .hasMessageContaining("Message contains unexpected fields"); - } - - @Test - void close_withDefaultHttpClient_closesSuccessfully() { - SnsMessageManager manager = SnsMessageManager.builder().build(); - - // Should not throw any exception - manager.close(); - } - - @Test - void close_multipleCallsToClose_handlesGracefully() { - SnsMessageManager manager = SnsMessageManager.builder().build(); - - // Multiple calls to close should not throw exceptions - manager.close(); - manager.close(); - manager.close(); - } - - @Test - void messageManagerConfiguration_builderPattern_worksCorrectly() { - Duration customTimeout = Duration.ofHours(2); - - MessageManagerConfiguration config = MessageManagerConfiguration.builder() - .certificateCacheTimeout(customTimeout) - .build(); - - assertThat(config.certificateCacheTimeout()).isEqualTo(customTimeout); - assertThat(config.httpClient()).isNull(); // Default should be null - } - - @Test - void messageManagerConfiguration_toBuilder_preservesValues() { - Duration originalTimeout = Duration.ofMinutes(30); - - MessageManagerConfiguration original = MessageManagerConfiguration.builder() - .certificateCacheTimeout(originalTimeout) - .build(); - - MessageManagerConfiguration copy = original.toBuilder() - .certificateCacheTimeout(Duration.ofHours(1)) - .build(); - - assertThat(original.certificateCacheTimeout()).isEqualTo(originalTimeout); - assertThat(copy.certificateCacheTimeout()).isEqualTo(Duration.ofHours(1)); - } - - @Test - void messageManagerConfiguration_equalsAndHashCode_workCorrectly() { - Duration timeout = Duration.ofMinutes(10); - - MessageManagerConfiguration config1 = MessageManagerConfiguration.builder() - .certificateCacheTimeout(timeout) - .build(); - - MessageManagerConfiguration config2 = MessageManagerConfiguration.builder() - .certificateCacheTimeout(timeout) - .build(); - - MessageManagerConfiguration config3 = MessageManagerConfiguration.builder() - .certificateCacheTimeout(Duration.ofMinutes(20)) - .build(); - - assertThat(config1).isEqualTo(config2); - assertThat(config1).isNotEqualTo(config3); - assertThat(config1.hashCode()).isEqualTo(config2.hashCode()); - assertThat(config1.hashCode()).isNotEqualTo(config3.hashCode()); - } - - @Test - void messageManagerConfiguration_toString_containsExpectedFields() { - Duration timeout = Duration.ofMinutes(5); - - MessageManagerConfiguration config = MessageManagerConfiguration.builder() - .certificateCacheTimeout(timeout) - .build(); - - String toString = config.toString(); - assertThat(toString).contains("MessageManagerConfiguration"); - assertThat(toString).contains("certificateCacheTimeout"); - } -} \ No newline at end of file From da66f00fd09994fbaefd1c968b38fa01c7523e45 Mon Sep 17 00:00:00 2001 From: Dongie Agnir Date: Mon, 30 Mar 2026 13:53:34 -0700 Subject: [PATCH 2/2] Update version in POM Parent version changed after merge from master --- services-custom/sns-message-manager/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services-custom/sns-message-manager/pom.xml b/services-custom/sns-message-manager/pom.xml index f3aa952f2541..d99a51798201 100644 --- a/services-custom/sns-message-manager/pom.xml +++ b/services-custom/sns-message-manager/pom.xml @@ -21,7 +21,7 @@ software.amazon.awssdk aws-sdk-java-pom - 2.42.15-SNAPSHOT + 2.42.25-SNAPSHOT ../../pom.xml sns-message-manager