You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Testcontainers与LocalStack的AuthService集成测试配置求助

如何为AWS Cognito同步用户的AuthService编写正确的集成测试?

我正在尝试为将新用户同步到AWS Cognito用户池的AuthService编写集成测试,目前测试配置无法正常运行,需要调整配置以完成测试。以下是相关代码和配置:

业务代码(AuthService.java)

@Service
@RequiredArgsConstructor
public class AuthService {

    /**
     * The algorithm used for HMAC-SHA256 hashing.
     */
    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";

    /**
     * The client for interacting with Amazon Cognito Identity Provider.
     */
    private final CognitoIdentityProviderClient client;

    /**
     * The AWS properties containing necessary configuration.
     */
    private final AwsProperties properties;

    /**
     * Signs up a user using the provided SignUpDto.
     *
     * @param requestDto The SignUpDto containing user signup information.
     */
    public void signUp(SignUpDto requestDto) {
        final AttributeType attributeType = AttributeType.builder()
                .name("email")
                .value(requestDto.email())
                .build();

        final String secretVal = calculateSecretHash(properties.getClientId(),
                properties.getSecretKey(),
                requestDto.email());
        final SignUpRequest request = SignUpRequest.builder()
                .userAttributes(List.of(attributeType))
                .username(requestDto.email())
                .password(requestDto.password())
                .clientId(properties.getClientId())
                .secretHash(secretVal)
                .build();

        client.signUp(request);
    }

    /**
     * Confirms user signup using the provided UserConfirmationDto.
     *
     * @param confirmation The UserConfirmationDto containing user confirmation information.
     */
    public void confirmSignUp(UserConfirmationDto confirmation) {
        final ConfirmSignUpRequest req;
        req = ConfirmSignUpRequest.builder()
                .clientId(properties.getClientId())
                .confirmationCode(confirmation.confirmationCode())
                .username(confirmation.email())
                .secretHash(calculateSecretHash(properties.getClientId(),
                        properties.getSecretKey(),
                        confirmation.email()))
                .build();


        client.confirmSignUp(req);
    }

    /**
     * Calculates the secret hash for the provided user pool client ID, client secret, and email.
     *
     * @param userPoolClientId     The user pool client ID.
     * @param userPoolClientSecret The user pool client secret.
     * @param email                The user's email.
     * @return The calculated secret hash.
     * @throws SecretHashException If an error occurs during secret hash calculation.
     */
    private String calculateSecretHash(String userPoolClientId, String userPoolClientSecret, String email) {
        final SecretKeySpec signingKey = new SecretKeySpec(
                userPoolClientSecret.getBytes(StandardCharsets.UTF_8),
                HMAC_SHA256_ALGORITHM);

        final Mac mac;
        try {
            mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
            mac.init(signingKey);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            throw new SecretHashException(e.getMessage());
        }

        mac.update(email.getBytes(StandardCharsets.UTF_8));
        final byte[] rawHmac = mac.doFinal(userPoolClientId.getBytes(StandardCharsets.UTF_8));
        return java.util.Base64.getEncoder().encodeToString(rawHmac);
    }
}

build.gradle配置

plugins {
    id 'java'
    id 'checkstyle'
    alias(libs.plugins.spring.boot)
    alias(libs.plugins.dependency.management)
}

group = 'java.com.mykytaaa'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(libs.spring.starter.weblux)
    compileOnly(libs.lombok)
    annotationProcessor(libs.lombok)
    testImplementation(libs.spring.starter.test)
    testImplementation(libs.reactor)
    implementation(libs.mapstruct)
    annotationProcessor(libs.mapstruct.processor)

    testImplementation 'org.testcontainers:localstack:1.19.7'
    testImplementation 'org.testcontainers:junit-jupiter:1.19.7'

    testCompileOnly 'org.projectlombok:lombok:1.18.30'
    testAnnotationProcessor 'org.projectlombok:lombok:1.18.30'


    runtimeOnly(libs.awssdk.bom)
    implementation(libs.awssdk.cognitoidentityprovider)
}

tasks.named('test') {
    useJUnitPlatform()
}

当前测试代码

AbstractIntegrationTestBase.java

@SpringBootTest
@ActiveProfiles("test")
public abstract class AbstractIntegrationTestBase {

    public static LocalStackContainer localStack = new LocalStackContainer(DockerImageName.parse("localstack/localstack"));

    @BeforeAll
    static void runContainer() {
        localStack.start();
    }

}

AwsConfiguration.java

@Configuration
public class AwsConfiguration {

    /**
     * Creates and configures an instance of CognitoIdentityProviderClient.
     *
     * @param region The AWS region used for configuration.
     * @return An instance of CognitoIdentityProviderClient.
     */
    @Bean
    public CognitoIdentityProviderClient identityProviderClient(@Value("${aws.region}") Region region) {
        return CognitoIdentityProviderClient.builder()
                .endpointOverride(AbstractIntegrationTestBase.localStack.getEndpoint())
                .region(region)
                .build();
    }
}

AuthServiceIntegrationTest.java

@RequiredArgsConstructor
@ExtendWith(SoftAssertionsExtension.class)
public class AuthServiceIntegrationTest extends AbstractIntegrationTestBase {

    private final AuthService authService;

    @Test
    void shouldSuccessfullySignUpUser(SoftAssertions softly) {
        final SignUpDto signUpDto = new SignUpDto(
                "exampleemail@gmail.com",
                "qQ-123456789"
        );

        authService.signUp(signUpDto);
    }
}

调整方案

1. 修正LocalStack容器配置

使用Testcontainers官方注解管理容器生命周期,指定启用Cognito服务并设置必要环境变量:

@Testcontainers
@SpringBootTest
@ActiveProfiles("test")
public abstract class AbstractIntegrationTestBase {

    @Container
    public static final LocalStackContainer localStack = new LocalStackContainer(DockerImageName.parse("localstack/localstack:1.19.7"))
            .withServices(LocalStackContainer.Service.COGNITO_IDP)
            .withEnv("AWS_DEFAULT_REGION", "us-east-1")
            .withEnv("AWS_ACCESS_KEY_ID", "test")
            .withEnv("AWS_SECRET_ACCESS_KEY", "test");
}
  • 用@Testcontainers和@Container自动管理容器启停,无需手动调用start()
  • 指定COGNITO_IDP服务,避免启动所有LocalStack服务
  • 设置测试专用的AWS凭证和区域,符合LocalStack要求

2. 调整AWS客户端配置

移除对测试基类的硬依赖,使用容器提供的endpoint和测试凭证:

@Configuration
public class AwsConfiguration {

    @Bean
    public CognitoIdentityProviderClient identityProviderClient(LocalStackContainer localStack) {
        AwsCredentials credentials = AwsBasicCredentials.create("test", "test");

        return CognitoIdentityProviderClient.builder()
                .endpointOverride(localStack.getEndpointOverride(LocalStackContainer.Service.COGNITO_IDP))
                .credentialsProvider(StaticCredentialsProvider.create(credentials))
                .region(Region.of(localStack.getEnvMap().get("AWS_DEFAULT_REGION")))
                .build();
    }
}
  • 注入LocalStackContainer实例,动态获取Cognito专属endpoint(LocalStack各服务endpoint不同)
  • 使用静态测试凭证,避免依赖外部配置
  • 从容器环境变量中获取区域,保持配置统一

3. 测试前初始化Cognito资源

LocalStack启动时无任何预配置资源,必须先创建用户池和客户端才能执行注册操作:

@RequiredArgsConstructor
@ExtendWith(SoftAssertionsExtension.class)
@SpringBootTest
@ActiveProfiles("test")
public class AuthServiceIntegrationTest extends AbstractIntegrationTestBase {

    private final AuthService authService;
    private final CognitoIdentityProviderClient cognitoClient;
    private final AwsProperties awsProperties;

    @BeforeAll
    void setupCognitoResources() {
        // 创建用户池
        CreateUserPoolResponse userPoolResponse = cognitoClient.createUserPool(CreateUserPoolRequest.builder()
                .poolName("test-user-pool")
                .autoVerifiedAttributes(List.of("email"))
                .build());
        String userPoolId = userPoolResponse.userPool().id();

        // 创建带密钥的用户池客户端(业务代码需要计算secretHash)
        CreateUserPoolClientResponse clientResponse = cognitoClient.createUserPoolClient(CreateUserPoolClientRequest.builder()
                .userPoolId(userPoolId)
                .clientName("test-client")
                .generateSecret(true)
                .build());

        // 更新测试用AWS配置
        awsProperties.setClientId(clientResponse.userPoolClient().clientId());
        awsProperties.setSecretKey(clientResponse.userPoolClient().clientSecret());
        awsProperties.setUserPoolId(userPoolId);
        awsProperties.setRegion(Region.of(localStack.getEnvMap().get("AWS_DEFAULT_REGION")));
    }

    @Test
    void shouldSuccessfullySignUpUser(SoftAssertions softly) {
        final SignUpDto signUpDto = new SignUpDto(
                "exampleemail@gmail.com",
                "qQ-123456789"
        );

        // 执行注册
        authService.signUp(signUpDto);

        // 验证用户是否成功创建
        AdminGetUserResponse userResponse = cognitoClient.adminGetUser(AdminGetUserRequest.builder()
                .userPoolId(awsProperties.getUserPoolId())
                .username(signUpDto.email())
                .build());

        softly.assertThat(userResponse.username()).isEqualTo(signUpDto.email());
        softly.assertThat(userResponse.userAttributes()).anyMatch(attr -> attr.name().equals("email") && attr.value().equals(signUpDto.email()));
    }
}
  • 在@BeforeAll中创建用户池和带密钥的客户端,匹配业务代码的secretHash计算需求
  • 更新AwsProperties配置,让AuthService使用测试环境资源
  • 添加验证逻辑,确认用户成功注册到Cognito

4. 补充测试配置文件

在src/test/resources/application-test.yml中添加基础配置:

aws:
  region: us-east-1

5. 完善AwsProperties类

确保包含必要的配置字段:

@ConfigurationProperties(prefix = "aws")
@Data
public class AwsProperties {
    private String region;
    private String clientId;
    private String secretKey;
    private String userPoolId; // 新增字段,存储测试用户池ID
}

并在主配置类中添加@EnableConfigurationProperties(AwsProperties.class)注解,启用配置绑定。


内容的提问来源于stack exchange,提问作者ZED

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 04:47:03