From f3480b6ba0429035fbfe52adf06005745a1b8d95 Mon Sep 17 00:00:00 2001 From: irving Date: Fri, 31 Oct 2025 20:40:21 -0400 Subject: [PATCH] =?UTF-8?q?feat(apitest):=20=E6=96=B0=E5=A2=9E=20API=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=8E=AF=E5=A2=83=E4=B8=8E=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 apitest 专用 MySQL 配置与 Docker 编排(docker/apitest-mysql.yml、docker/apitest-mysql/) - 增加 ApiTestSecurityConfig / ApiTestSecurityProperties 与 ApiTestAuthenticationFilter - 新增 application-apitest.yml 与相关测试目录(play-admin/src/test/java/com/starry/admin/api/) - 调整根 pom 与 play-admin/pom 依赖,优化 SpringSecurityConfig 以兼容 apitest --- docker/apitest-mysql.yml | 27 ++ .../init/010-grant-performance-schema.sql | 2 + docker/apitest-mysql/init/README.md | 10 + play-admin/pom.xml | 20 ++ .../common/apitest/ApiTestDataSeeder.java | 318 ++++++++++++++++++ .../config/ApiTestSecurityConfig.java | 49 +++ .../config/ApiTestSecurityProperties.java | 73 ++++ .../security/config/SpringSecurityConfig.java | 2 + .../filter/ApiTestAuthenticationFilter.java | 96 ++++++ .../main/resources/application-apitest.yml | 82 +++++ .../com/starry/admin/api/AbstractApiTest.java | 21 ++ .../admin/api/HealthControllerApiTest.java | 20 ++ .../SysTenantPackageControllerApiTest.java | 22 ++ pom.xml | 10 +- 14 files changed, 751 insertions(+), 1 deletion(-) create mode 100644 docker/apitest-mysql.yml create mode 100644 docker/apitest-mysql/init/010-grant-performance-schema.sql create mode 100644 docker/apitest-mysql/init/README.md create mode 100644 play-admin/src/main/java/com/starry/admin/common/apitest/ApiTestDataSeeder.java create mode 100644 play-admin/src/main/java/com/starry/admin/common/security/config/ApiTestSecurityConfig.java create mode 100644 play-admin/src/main/java/com/starry/admin/common/security/config/ApiTestSecurityProperties.java create mode 100644 play-admin/src/main/java/com/starry/admin/common/security/filter/ApiTestAuthenticationFilter.java create mode 100644 play-admin/src/main/resources/application-apitest.yml create mode 100644 play-admin/src/test/java/com/starry/admin/api/AbstractApiTest.java create mode 100644 play-admin/src/test/java/com/starry/admin/api/HealthControllerApiTest.java create mode 100644 play-admin/src/test/java/com/starry/admin/api/SysTenantPackageControllerApiTest.java diff --git a/docker/apitest-mysql.yml b/docker/apitest-mysql.yml new file mode 100644 index 0000000..051a4e0 --- /dev/null +++ b/docker/apitest-mysql.yml @@ -0,0 +1,27 @@ +version: "3.9" + +services: + mysql-apitest: + image: mysql:8.0.32 + container_name: peipei-mysql-apitest + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: peipei_apitest + MYSQL_USER: apitest + MYSQL_PASSWORD: apitest + ports: + - "33306:3306" + volumes: + - ./apitest-mysql/init:/docker-entrypoint-initdb.d:ro + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"] + interval: 10s + timeout: 5s + retries: 10 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--lower_case_table_names=1" + - "--explicit_defaults_for_timestamp=1" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" diff --git a/docker/apitest-mysql/init/010-grant-performance-schema.sql b/docker/apitest-mysql/init/010-grant-performance-schema.sql new file mode 100644 index 0000000..3151403 --- /dev/null +++ b/docker/apitest-mysql/init/010-grant-performance-schema.sql @@ -0,0 +1,2 @@ +GRANT SELECT ON performance_schema.* TO 'apitest'@'%'; +FLUSH PRIVILEGES; diff --git a/docker/apitest-mysql/init/README.md b/docker/apitest-mysql/init/README.md new file mode 100644 index 0000000..164e71a --- /dev/null +++ b/docker/apitest-mysql/init/README.md @@ -0,0 +1,10 @@ +# API Test MySQL Seed Files + +将初始化 schema 和种子数据的 SQL 文件放在此目录下,文件会在 `mysql-apitest` 容器启动时自动执行。 + +推荐约定: +- `000-schema.sql`:创建数据库/表结构(可复用 Flyway 生成的整库脚本)。 +- `100-seed-*.sql`:插入基础租户、用户、商品、优惠券等测试数据。 +- `900-cleanup.sql`:可选的清理脚本,用于重置状态。 + +容器销毁(`docker-compose down -v`)后数据会一起删除,保证每次测试环境一致。 diff --git a/play-admin/pom.xml b/play-admin/pom.xml index 8938048..c846fcc 100644 --- a/play-admin/pom.xml +++ b/play-admin/pom.xml @@ -16,6 +16,7 @@ 11 11 UTF-8 + test @@ -173,6 +174,22 @@ json-path test + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.security + spring-security-test + test + @@ -203,6 +220,9 @@ 3.0.0-M7 false + + ${spring.profiles.active} + diff --git a/play-admin/src/main/java/com/starry/admin/common/apitest/ApiTestDataSeeder.java b/play-admin/src/main/java/com/starry/admin/common/apitest/ApiTestDataSeeder.java new file mode 100644 index 0000000..d3ef6bc --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/common/apitest/ApiTestDataSeeder.java @@ -0,0 +1,318 @@ +package com.starry.admin.common.apitest; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.starry.admin.modules.clerk.module.entity.PlayClerkCommodityEntity; +import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity; +import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity; +import com.starry.admin.modules.clerk.service.IPlayClerkCommodityService; +import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService; +import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService; +import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity; +import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService; +import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity; +import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService; +import com.starry.admin.modules.shop.module.entity.PlayCommodityInfoEntity; +import com.starry.admin.modules.shop.service.IPlayCommodityInfoService; +import com.starry.admin.modules.system.module.entity.SysTenantEntity; +import com.starry.admin.modules.system.module.entity.SysTenantPackageEntity; +import com.starry.admin.modules.system.module.entity.SysUserEntity; +import com.starry.admin.modules.system.service.ISysTenantPackageService; +import com.starry.admin.modules.system.service.ISysTenantService; +import com.starry.admin.modules.system.service.SysUserService; +import com.starry.admin.utils.SecurityUtils; +import com.starry.common.context.CustomSecurityContextHolder; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@Profile("apitest") +public class ApiTestDataSeeder implements CommandLineRunner { + + private static final Logger log = LoggerFactory.getLogger(ApiTestDataSeeder.class); + + public static final String DEFAULT_PACKAGE_ID = "pkg-basic"; + public static final String DEFAULT_TENANT_ID = "tenant-apitest"; + public static final String DEFAULT_TENANT_KEY = "tenant-key-apitest"; + public static final String DEFAULT_TENANT_NAME = "API Test Tenant"; + public static final String DEFAULT_ADMIN_USER_ID = "user-apitest-admin"; + public static final String DEFAULT_ADMIN_USERNAME = "apitest-admin"; + public static final String DEFAULT_GROUP_ID = "group-basic"; + public static final String DEFAULT_CLERK_LEVEL_ID = "lvl-basic"; + public static final String DEFAULT_CLERK_ID = "clerk-apitest"; + public static final String DEFAULT_CLERK_OPEN_ID = "openid-clerk-apitest"; + public static final String DEFAULT_COMMODITY_ID = "svc-basic"; + public static final String DEFAULT_CLERK_COMMODITY_ID = "clerk-svc-basic"; + public static final String DEFAULT_CUSTOMER_ID = "customer-apitest"; + + private final ISysTenantPackageService tenantPackageService; + private final ISysTenantService tenantService; + private final SysUserService sysUserService; + private final IPlayPersonnelGroupInfoService personnelGroupInfoService; + private final IPlayClerkLevelInfoService clerkLevelInfoService; + private final IPlayClerkUserInfoService clerkUserInfoService; + private final IPlayCommodityInfoService commodityInfoService; + private final IPlayClerkCommodityService clerkCommodityService; + private final IPlayCustomUserInfoService customUserInfoService; + private final PasswordEncoder passwordEncoder; + + public ApiTestDataSeeder( + ISysTenantPackageService tenantPackageService, + ISysTenantService tenantService, + SysUserService sysUserService, + IPlayPersonnelGroupInfoService personnelGroupInfoService, + IPlayClerkLevelInfoService clerkLevelInfoService, + IPlayClerkUserInfoService clerkUserInfoService, + IPlayCommodityInfoService commodityInfoService, + IPlayClerkCommodityService clerkCommodityService, + IPlayCustomUserInfoService customUserInfoService, + PasswordEncoder passwordEncoder) { + this.tenantPackageService = tenantPackageService; + this.tenantService = tenantService; + this.sysUserService = sysUserService; + this.personnelGroupInfoService = personnelGroupInfoService; + this.clerkLevelInfoService = clerkLevelInfoService; + this.clerkUserInfoService = clerkUserInfoService; + this.commodityInfoService = commodityInfoService; + this.clerkCommodityService = clerkCommodityService; + this.customUserInfoService = customUserInfoService; + this.passwordEncoder = passwordEncoder; + } + + @Override + @Transactional + public void run(String... args) { + seedTenantPackage(); + seedTenant(); + + String originalTenant = SecurityUtils.getTenantId(); + try { + SecurityUtils.setTenantId(DEFAULT_TENANT_ID); + seedAdminUser(); + seedPersonnelGroup(); + seedClerkLevel(); + seedCommodity(); + seedClerk(); + seedClerkCommodity(); + seedCustomer(); + } finally { + if (Objects.nonNull(originalTenant)) { + SecurityUtils.setTenantId(originalTenant); + } + CustomSecurityContextHolder.remove(); + } + } + + private void seedTenantPackage() { + long existing = tenantPackageService.count(Wrappers.lambdaQuery() + .eq(SysTenantPackageEntity::getPackageId, DEFAULT_PACKAGE_ID)); + if (existing > 0) { + log.info("API test tenant package {} already exists", DEFAULT_PACKAGE_ID); + return; + } + + SysTenantPackageEntity entity = new SysTenantPackageEntity(); + entity.setPackageId(DEFAULT_PACKAGE_ID); + entity.setPackageName("API测试基础套餐"); + entity.setStatus("0"); + entity.setMenuIds("[]"); + entity.setRemarks("Seeded for API integration tests"); + tenantPackageService.save(entity); + log.info("Inserted API test tenant package {}", DEFAULT_PACKAGE_ID); + } + + private void seedTenant() { + SysTenantEntity tenant = tenantService.getById(DEFAULT_TENANT_ID); + if (tenant != null) { + log.info("API test tenant {} already exists", DEFAULT_TENANT_ID); + return; + } + + SysTenantEntity entity = new SysTenantEntity(); + entity.setTenantId(DEFAULT_TENANT_ID); + entity.setTenantName(DEFAULT_TENANT_NAME); + entity.setTenantType("0"); + entity.setTenantStatus("0"); + entity.setTenantCode("apitest"); + entity.setTenantKey(DEFAULT_TENANT_KEY); + entity.setPackageId(DEFAULT_PACKAGE_ID); + entity.setTenantTime(new Date(System.currentTimeMillis() + 365L * 24 * 3600 * 1000)); + entity.setUserName(DEFAULT_ADMIN_USERNAME); + entity.setUserPwd(passwordEncoder.encode("apitest-secret")); + entity.setPhone("13800000000"); + entity.setEmail("apitest@example.com"); + entity.setAddress("API Test Street 1"); + tenantService.save(entity); + log.info("Inserted API test tenant {}", DEFAULT_TENANT_ID); + } + + private void seedAdminUser() { + SysUserEntity existing = sysUserService.getById(DEFAULT_ADMIN_USER_ID); + if (existing != null) { + log.info("API test admin user {} already exists", DEFAULT_ADMIN_USER_ID); + return; + } + + SysUserEntity admin = new SysUserEntity(); + admin.setUserId(DEFAULT_ADMIN_USER_ID); + admin.setUserCode(DEFAULT_ADMIN_USERNAME); + admin.setPassWord(passwordEncoder.encode("apitest-secret")); + admin.setRealName("API Test Admin"); + admin.setUserNickname("API Admin"); + admin.setStatus(0); + admin.setUserType(1); + admin.setTenantId(DEFAULT_TENANT_ID); + admin.setMobile("13800000000"); + admin.setAddTime(LocalDateTime.now()); + admin.setSuperAdmin(Boolean.TRUE); + sysUserService.save(admin); + log.info("Inserted API test admin user {}", DEFAULT_ADMIN_USER_ID); + } + + private void seedPersonnelGroup() { + PlayPersonnelGroupInfoEntity group = personnelGroupInfoService.getById(DEFAULT_GROUP_ID); + if (group != null) { + log.info("API test personnel group {} already exists", DEFAULT_GROUP_ID); + return; + } + + PlayPersonnelGroupInfoEntity entity = new PlayPersonnelGroupInfoEntity(); + entity.setId(DEFAULT_GROUP_ID); + entity.setTenantId(DEFAULT_TENANT_ID); + entity.setSysUserId(DEFAULT_ADMIN_USER_ID); + entity.setSysUserCode(DEFAULT_ADMIN_USERNAME); + entity.setGroupName("测试小组"); + entity.setLeaderName("API Admin"); + entity.setAddTime(LocalDateTime.now()); + personnelGroupInfoService.save(entity); + log.info("Inserted API test personnel group {}", DEFAULT_GROUP_ID); + } + + private void seedClerkLevel() { + PlayClerkLevelInfoEntity level = clerkLevelInfoService.getById(DEFAULT_CLERK_LEVEL_ID); + if (level != null) { + log.info("API test clerk level {} already exists", DEFAULT_CLERK_LEVEL_ID); + return; + } + + PlayClerkLevelInfoEntity entity = new PlayClerkLevelInfoEntity(); + entity.setId(DEFAULT_CLERK_LEVEL_ID); + entity.setTenantId(DEFAULT_TENANT_ID); + entity.setName("基础等级"); + entity.setLevel(1); + entity.setFirstRegularRatio(60); + entity.setNotFirstRegularRatio(50); + entity.setFirstRandomRadio(55); + entity.setNotFirstRandomRadio(45); + entity.setFirstRewardRatio(40); + entity.setNotFirstRewardRatio(35); + clerkLevelInfoService.save(entity); + log.info("Inserted API test clerk level {}", DEFAULT_CLERK_LEVEL_ID); + } + + private void seedCommodity() { + PlayCommodityInfoEntity commodity = commodityInfoService.getById(DEFAULT_COMMODITY_ID); + if (commodity != null) { + log.info("API test commodity {} already exists", DEFAULT_COMMODITY_ID); + return; + } + + PlayCommodityInfoEntity entity = new PlayCommodityInfoEntity(); + entity.setId(DEFAULT_COMMODITY_ID); + entity.setTenantId(DEFAULT_TENANT_ID); + entity.setItemType("service"); + entity.setItemName("60分钟语音陪聊"); + entity.setServiceDuration("60min"); + entity.setEnableStace("1"); + entity.setSort(1); + commodityInfoService.save(entity); + log.info("Inserted API test commodity {}", DEFAULT_COMMODITY_ID); + } + + private void seedClerk() { + PlayClerkUserInfoEntity clerk = clerkUserInfoService.getById(DEFAULT_CLERK_ID); + if (clerk != null) { + log.info("API test clerk {} already exists", DEFAULT_CLERK_ID); + return; + } + + PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity(); + entity.setId(DEFAULT_CLERK_ID); + entity.setTenantId(DEFAULT_TENANT_ID); + entity.setSysUserId(DEFAULT_ADMIN_USER_ID); + entity.setOpenid(DEFAULT_CLERK_OPEN_ID); + entity.setNickname("小测官"); + entity.setGroupId(DEFAULT_GROUP_ID); + entity.setLevelId(DEFAULT_CLERK_LEVEL_ID); + entity.setFixingLevel("1"); + entity.setSex("2"); + entity.setPhone("13900000001"); + entity.setWeiChatCode("apitest-clerk"); + entity.setAvatar("https://example.com/avatar.png"); + entity.setAccountBalance(BigDecimal.ZERO); + entity.setOnboardingState("1"); + entity.setListingState("1"); + entity.setDisplayState("1"); + entity.setRandomOrderState("1"); + entity.setClerkState("1"); + entity.setEntryTime(LocalDateTime.now()); + clerkUserInfoService.save(entity); + log.info("Inserted API test clerk {}", DEFAULT_CLERK_ID); + } + + private void seedClerkCommodity() { + PlayClerkCommodityEntity mapping = clerkCommodityService.getById(DEFAULT_CLERK_COMMODITY_ID); + if (mapping != null) { + log.info("API test clerk commodity {} already exists", DEFAULT_CLERK_COMMODITY_ID); + return; + } + + PlayClerkCommodityEntity entity = new PlayClerkCommodityEntity(); + entity.setId(DEFAULT_CLERK_COMMODITY_ID); + entity.setTenantId(DEFAULT_TENANT_ID); + entity.setClerkId(DEFAULT_CLERK_ID); + entity.setCommodityId(DEFAULT_COMMODITY_ID); + entity.setCommodityName("60分钟语音陪聊"); + entity.setEnablingState("1"); + entity.setSort(1); + clerkCommodityService.save(entity); + log.info("Inserted API test clerk commodity link {}", DEFAULT_CLERK_COMMODITY_ID); + } + + private void seedCustomer() { + PlayCustomUserInfoEntity customer = customUserInfoService.getById(DEFAULT_CUSTOMER_ID); + if (customer != null) { + log.info("API test customer {} already exists", DEFAULT_CUSTOMER_ID); + return; + } + + PlayCustomUserInfoEntity entity = new PlayCustomUserInfoEntity(); + entity.setId(DEFAULT_CUSTOMER_ID); + entity.setTenantId(DEFAULT_TENANT_ID); + entity.setOpenid("openid-customer-apitest"); + entity.setUnionid("unionid-customer-apitest"); + entity.setNickname("测试顾客"); + entity.setSex(1); + entity.setPhone("13700000002"); + entity.setWeiChatCode("apitest-customer"); + entity.setAccountBalance(new BigDecimal("200.00")); + entity.setAccumulatedRechargeAmount(new BigDecimal("200.00")); + entity.setAccumulatedConsumptionAmount(BigDecimal.ZERO); + entity.setAccountState("1"); + entity.setSubscribeState("1"); + entity.setPurchaseState("1"); + entity.setMobilePhoneState("1"); + entity.setRegistrationTime(new Date()); + entity.setLastLoginTime(new Date()); + customUserInfoService.save(entity); + log.info("Inserted API test customer {}", DEFAULT_CUSTOMER_ID); + } +} diff --git a/play-admin/src/main/java/com/starry/admin/common/security/config/ApiTestSecurityConfig.java b/play-admin/src/main/java/com/starry/admin/common/security/config/ApiTestSecurityConfig.java new file mode 100644 index 0000000..7849086 --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/common/security/config/ApiTestSecurityConfig.java @@ -0,0 +1,49 @@ +package com.starry.admin.common.security.config; + +import com.starry.admin.common.security.filter.ApiTestAuthenticationFilter; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +@Profile("apitest") +@EnableConfigurationProperties(ApiTestSecurityProperties.class) +public class ApiTestSecurityConfig extends WebSecurityConfigurerAdapter { + + private final ApiTestSecurityProperties properties; + + public ApiTestSecurityConfig(ApiTestSecurityProperties properties) { + this.properties = properties; + } + + @Bean + public ApiTestAuthenticationFilter apiTestAuthenticationFilter() { + return new ApiTestAuthenticationFilter(properties); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable() + .formLogin().disable() + .logout().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + .authorizeRequests().anyRequest().authenticated().and() + .addFilterBefore(apiTestAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/play-admin/src/main/java/com/starry/admin/common/security/config/ApiTestSecurityProperties.java b/play-admin/src/main/java/com/starry/admin/common/security/config/ApiTestSecurityProperties.java new file mode 100644 index 0000000..0f368af --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/common/security/config/ApiTestSecurityProperties.java @@ -0,0 +1,73 @@ +package com.starry.admin.common.security.config; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "apitest.security") +public class ApiTestSecurityProperties { + + private String tenantHeader = "X-Tenant"; + private String userHeader = "X-Test-User"; + private final Defaults defaults = new Defaults(); + + public String getTenantHeader() { + return tenantHeader; + } + + public void setTenantHeader(String tenantHeader) { + this.tenantHeader = tenantHeader; + } + + public String getUserHeader() { + return userHeader; + } + + public void setUserHeader(String userHeader) { + this.userHeader = userHeader; + } + + public Defaults getDefaults() { + return defaults; + } + + public static class Defaults { + + private String tenantId = "tenant-apitest"; + private String userId = "apitest-user"; + private List roles = new ArrayList<>(); + private List permissions = new ArrayList<>(); + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public List getPermissions() { + return permissions; + } + + public void setPermissions(List permissions) { + this.permissions = permissions; + } + } +} diff --git a/play-admin/src/main/java/com/starry/admin/common/security/config/SpringSecurityConfig.java b/play-admin/src/main/java/com/starry/admin/common/security/config/SpringSecurityConfig.java index b2678d0..0badf73 100644 --- a/play-admin/src/main/java/com/starry/admin/common/security/config/SpringSecurityConfig.java +++ b/play-admin/src/main/java/com/starry/admin/common/security/config/SpringSecurityConfig.java @@ -12,6 +12,7 @@ import java.util.Set; import javax.annotation.Resource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; @@ -31,6 +32,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) +@Profile("!apitest") public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Resource diff --git a/play-admin/src/main/java/com/starry/admin/common/security/filter/ApiTestAuthenticationFilter.java b/play-admin/src/main/java/com/starry/admin/common/security/filter/ApiTestAuthenticationFilter.java new file mode 100644 index 0000000..833e5dd --- /dev/null +++ b/play-admin/src/main/java/com/starry/admin/common/security/filter/ApiTestAuthenticationFilter.java @@ -0,0 +1,96 @@ +package com.starry.admin.common.security.filter; + +import com.starry.admin.common.domain.LoginUser; +import com.starry.admin.common.security.config.ApiTestSecurityProperties; +import com.starry.admin.modules.system.module.entity.SysUserEntity; +import com.starry.common.constant.SecurityConstants; +import com.starry.common.context.CustomSecurityContextHolder; +import java.io.IOException; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +public class ApiTestAuthenticationFilter extends OncePerRequestFilter { + + private final ApiTestSecurityProperties properties; + + public ApiTestAuthenticationFilter(ApiTestSecurityProperties properties) { + this.properties = properties; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String requestedUser = request.getHeader(properties.getUserHeader()); + String requestedTenant = request.getHeader(properties.getTenantHeader()); + + String userId = StringUtils.hasText(requestedUser) ? requestedUser : properties.getDefaults().getUserId(); + String tenantId = StringUtils.hasText(requestedTenant) ? requestedTenant : properties.getDefaults().getTenantId(); + + if (!StringUtils.hasText(userId) || !StringUtils.hasText(tenantId)) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType("application/json"); + response.getWriter().write("{\"code\":401,\"message\":\"Missing test user or tenant header\"}"); + response.getWriter().flush(); + return; + } + + try { + LoginUser loginUser = buildLoginUser(userId, tenantId); + + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null, + Collections.emptyList()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + CustomSecurityContextHolder.set(SecurityConstants.DETAILS_USER_ID, userId); + CustomSecurityContextHolder.set(SecurityConstants.DETAILS_USERNAME, userId); + CustomSecurityContextHolder.setTenantId(tenantId); + CustomSecurityContextHolder.setPermission(String.join(",", loginUser.getPermissions())); + + filterChain.doFilter(request, response); + } finally { + CustomSecurityContextHolder.remove(); + SecurityContextHolder.clearContext(); + } + } + + private LoginUser buildLoginUser(String userId, String tenantId) { + SysUserEntity sysUser = new SysUserEntity(); + sysUser.setUserId(userId); + sysUser.setUserCode(userId); + sysUser.setRealName(userId); + sysUser.setTenantId(tenantId); + sysUser.setSuperAdmin(Boolean.FALSE); + sysUser.setStatus(0); + + LoginUser loginUser = new LoginUser(); + loginUser.setUser(sysUser); + loginUser.setUserId(userId); + loginUser.setUserName(userId); + loginUser.setToken("apitest-" + userId + "-" + tenantId); + loginUser.setLoginTime(System.currentTimeMillis()); + loginUser.setExpireTime(System.currentTimeMillis() + 3600_000); + loginUser.setTenantEndDate(new Date(System.currentTimeMillis() + 3600_000)); + loginUser.setTenantStatus(0); + + Set roles = new HashSet<>(properties.getDefaults().getRoles()); + Set permissions = new HashSet<>(properties.getDefaults().getPermissions()); + loginUser.setRoles(roles); + loginUser.setPermissions(permissions); + loginUser.setCurrentRole(roles.stream().findFirst().orElse(null)); + + return loginUser; + } +} diff --git a/play-admin/src/main/resources/application-apitest.yml b/play-admin/src/main/resources/application-apitest.yml new file mode 100644 index 0000000..116a226 --- /dev/null +++ b/play-admin/src/main/resources/application-apitest.yml @@ -0,0 +1,82 @@ +spring: + application: + name: admin-tenant-apitest + flyway: + table: admin_flyway_schema_history + baseline-on-migrate: true + baseline-version: 1 + enabled: true + locations: classpath:db/migration + clean-disabled: false + validate-on-migrate: false + out-of-order: false + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:33306/peipei_apitest?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true + username: apitest + password: apitest + druid: + enable: true + db-type: mysql + filters: stat,wall + max-active: 20 + initial-size: 1 + max-wait: 60000 + min-idle: 1 + time-between-eviction-runs-millis: 60000 + min-evictable-idle-time-millis: 300000 + validation-query: select 'x' + test-while-idle: true + test-on-borrow: false + test-on-return: false + pool-prepared-statements: true + max-open-prepared-statements: 20 + web-stat-filter: + enabled: false + stat-view-servlet: + enabled: false + redis: + host: 127.0.0.1 + database: 0 + port: 36379 + password: + timeout: 3000ms + task: + scheduling: + enabled: false + execution: + shutdown: + await-termination: true + await-termination-period: 5s +logging: + level: + root: info + com.starry: debug + +jwt: + tokenHeader: X-Test-Token + tokenHead: Bearer + secret: apitest-secret + expiration: 360000 + +token: + header: Authorization + secret: apitest-override-secret + expireTime: 60 + +xl: + login: + authCode: + enable: false + +apitest: + security: + tenant-header: X-Tenant + user-header: X-Test-User + defaults: + tenant-id: tenant-apitest + user-id: apitest-user + roles: + - ROLE_TESTER + permissions: [] diff --git a/play-admin/src/test/java/com/starry/admin/api/AbstractApiTest.java b/play-admin/src/test/java/com/starry/admin/api/AbstractApiTest.java new file mode 100644 index 0000000..4d47997 --- /dev/null +++ b/play-admin/src/test/java/com/starry/admin/api/AbstractApiTest.java @@ -0,0 +1,21 @@ +package com.starry.admin.api; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +@ActiveProfiles("apitest") +public abstract class AbstractApiTest { + + protected static final String TENANT_HEADER = "X-Tenant"; + protected static final String USER_HEADER = "X-Test-User"; + protected static final String DEFAULT_TENANT = "tenant-apitest"; + protected static final String DEFAULT_USER = "apitest-user"; + + @Autowired + protected MockMvc mockMvc; +} diff --git a/play-admin/src/test/java/com/starry/admin/api/HealthControllerApiTest.java b/play-admin/src/test/java/com/starry/admin/api/HealthControllerApiTest.java new file mode 100644 index 0000000..ec98142 --- /dev/null +++ b/play-admin/src/test/java/com/starry/admin/api/HealthControllerApiTest.java @@ -0,0 +1,20 @@ +package com.starry.admin.api; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; + +class HealthControllerApiTest extends AbstractApiTest { + + @Test + void pingReturnsPong() throws Exception { + mockMvc.perform(get("/health/ping") + .header(USER_HEADER, DEFAULT_USER) + .header(TENANT_HEADER, DEFAULT_TENANT)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").value("pong")); + } +} diff --git a/play-admin/src/test/java/com/starry/admin/api/SysTenantPackageControllerApiTest.java b/play-admin/src/test/java/com/starry/admin/api/SysTenantPackageControllerApiTest.java new file mode 100644 index 0000000..1e1fa1e --- /dev/null +++ b/play-admin/src/test/java/com/starry/admin/api/SysTenantPackageControllerApiTest.java @@ -0,0 +1,22 @@ +package com.starry.admin.api; + +import static org.hamcrest.Matchers.hasItem; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; + +class SysTenantPackageControllerApiTest extends AbstractApiTest { + + @Test + void getSimpleListReturnsSeededPackage() throws Exception { + mockMvc.perform(get("/platform/package/get-simple-list") + .header(USER_HEADER, DEFAULT_USER) + .header(TENANT_HEADER, DEFAULT_TENANT)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data[*].id", hasItem("pkg-basic"))); + } +} diff --git a/pom.xml b/pom.xml index 285fc3f..bc17f04 100644 --- a/pom.xml +++ b/pom.xml @@ -333,6 +333,14 @@ + + apitest + + apitest + 33306 + + + osx-arm64 @@ -369,4 +377,4 @@ - \ No newline at end of file +