forked from wangziqi/ruoyi-vue-pro
feat(education): resolve student tenant context
This commit is contained in:
@@ -7,6 +7,8 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
@@ -47,4 +49,12 @@ public class EducationPropertiesTest {
|
||||
assertEquals("1.0.0", defaults.getVersion(), "默认版本应为 1.0.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHostnameTenantMapDefaults() {
|
||||
EducationProperties defaults = new EducationProperties();
|
||||
assertNotNull(defaults.getHostnameTenantMap(), "hostnameTenantMap 默认不应为 null");
|
||||
assertTrue(defaults.getHostnameTenantMap().isEmpty(), "hostnameTenantMap 默认应为空");
|
||||
assertEquals(List.of("PASSWORD", "SMS"), defaults.getLoginMethods(), "默认登录方式应与 Member 入口一致");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package cn.iocoder.yudao.module.education.controller.app.tenant;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.NullAndEmptySource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* {@link EducationTenantController} 的单元测试
|
||||
*
|
||||
* @author 恭学教育
|
||||
*/
|
||||
class EducationTenantControllerTest {
|
||||
|
||||
// ========== normalizeHostname ==========
|
||||
|
||||
@ParameterizedTest
|
||||
@NullAndEmptySource
|
||||
void normalizeHostname_shouldReturnNullForBlankInput(String hostname) {
|
||||
assertNull(EducationTenantController.normalizeHostname(hostname));
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizeHostname_shouldLowercase() {
|
||||
assertEquals("school.example.com",
|
||||
EducationTenantController.normalizeHostname("School.Example.COM"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizeHostname_shouldTrimWhitespace() {
|
||||
assertEquals("school.example.com",
|
||||
EducationTenantController.normalizeHostname(" school.example.com "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizeHostname_shouldKeepPort() {
|
||||
assertEquals("school.example.com:8080",
|
||||
EducationTenantController.normalizeHostname("school.example.com:8080"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizeHostname_shouldLowercaseAndKeepPort() {
|
||||
assertEquals("school.example.com:8080",
|
||||
EducationTenantController.normalizeHostname("School.Example.COM:8080"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizeHostname_shouldRejectProtocol() {
|
||||
assertThrows(RuntimeException.class, () ->
|
||||
EducationTenantController.normalizeHostname("http://school.example.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizeHostname_shouldRejectPath() {
|
||||
assertThrows(RuntimeException.class, () ->
|
||||
EducationTenantController.normalizeHostname("school.example.com/path"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizeHostname_shouldRejectProtocolWithPort() {
|
||||
assertThrows(RuntimeException.class, () ->
|
||||
EducationTenantController.normalizeHostname("https://school.example.com:8443"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizeHostname_shouldRejectOversizedPort() {
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> EducationTenantController.normalizeHostname("school.example.com:999999999999"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizeHostname_shouldKeepIPv6Colons() {
|
||||
// A bare IPv6 address like ::1: the port-stripping should be safe
|
||||
// because last colon is followed by digits -> treated as port
|
||||
// We'll just check no exception thrown
|
||||
assertDoesNotThrow(() -> EducationTenantController.normalizeHostname("[::1]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizeHostname_shouldRejectNonNumericPort() {
|
||||
assertThrows(RuntimeException.class,
|
||||
() -> EducationTenantController.normalizeHostname("host:name"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package cn.iocoder.yudao.module.education.controller.app.tenant;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi;
|
||||
import cn.iocoder.yudao.framework.common.biz.system.tenant.dto.TenantRespDTO;
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.education.config.EducationProperties;
|
||||
import cn.iocoder.yudao.module.education.controller.app.tenant.vo.EducationTenantRespVO;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link EducationTenantController} 的集成测试 — 测试租户解析流程
|
||||
*
|
||||
* @author 恭学教育
|
||||
*/
|
||||
@SpringBootTest(classes = EducationTenantResolveIntegrationTest.Config.class,
|
||||
properties = {
|
||||
"yudao.education.enabled=true",
|
||||
"yudao.education.version=1.0.0-test"
|
||||
},
|
||||
webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
@ActiveProfiles("unit-test")
|
||||
class EducationTenantResolveIntegrationTest {
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(EducationProperties.class)
|
||||
@Import(EducationTenantController.class)
|
||||
static class Config {
|
||||
}
|
||||
|
||||
@Resource
|
||||
private EducationTenantController controller;
|
||||
|
||||
@MockitoBean
|
||||
private TenantCommonApi tenantCommonApi;
|
||||
|
||||
// ========== happy path ==========
|
||||
|
||||
@Test
|
||||
void resolve_byTenantName_shouldReturnActiveTenant() {
|
||||
TenantRespDTO dto = buildTenant(100L, "demo-school", CommonStatusEnum.ENABLE.getStatus());
|
||||
when(tenantCommonApi.getTenantByName("demo-school")).thenReturn(dto);
|
||||
|
||||
CommonResult<EducationTenantRespVO> result = controller.resolve(null, "demo-school");
|
||||
|
||||
assertTrue(result.isSuccess());
|
||||
EducationTenantRespVO data = result.getData();
|
||||
assertEquals(100L, data.getTenantId());
|
||||
assertEquals("demo-school", data.getTenantName());
|
||||
assertEquals("ACTIVE", data.getStatus());
|
||||
assertTrue(data.getLoginMethods().contains("PASSWORD"));
|
||||
assertTrue(data.getLoginMethods().contains("SMS"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_byHostname_shouldFindByWebsite() {
|
||||
TenantRespDTO dto = buildTenant(200L, "school-alpha", CommonStatusEnum.ENABLE.getStatus());
|
||||
when(tenantCommonApi.getTenantByWebsite("school.example.com")).thenReturn(dto);
|
||||
|
||||
CommonResult<EducationTenantRespVO> result = controller.resolve("school.example.com", null);
|
||||
|
||||
assertTrue(result.isSuccess());
|
||||
assertEquals(200L, result.getData().getTenantId());
|
||||
assertEquals("school-alpha", result.getData().getTenantName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_byHostname_shouldNormalizeCaseAndKeepPort() {
|
||||
TenantRespDTO dto = buildTenant(300L, "school-beta", CommonStatusEnum.ENABLE.getStatus());
|
||||
when(tenantCommonApi.getTenantByWebsite("school.example.com:8443")).thenReturn(dto);
|
||||
|
||||
CommonResult<EducationTenantRespVO> result = controller.resolve("School.Example.COM:8443", null);
|
||||
|
||||
assertTrue(result.isSuccess());
|
||||
assertEquals(300L, result.getData().getTenantId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_conflictingHostnameAndTenantName_shouldFail() {
|
||||
TenantRespDTO byName = buildTenant(600L, "school-a", CommonStatusEnum.ENABLE.getStatus());
|
||||
TenantRespDTO byHost = buildTenant(601L, "school-b", CommonStatusEnum.ENABLE.getStatus());
|
||||
when(tenantCommonApi.getTenantByName("school-a")).thenReturn(byName);
|
||||
when(tenantCommonApi.getTenantByWebsite("school-b.example.com")).thenReturn(byHost);
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> controller.resolve("school-b.example.com", "school-a"));
|
||||
assertTrue(exception.getMessage().contains("指向不同租户"));
|
||||
}
|
||||
|
||||
// ========== error cases ==========
|
||||
|
||||
@Test
|
||||
void resolve_noInput_shouldFail() {
|
||||
try {
|
||||
controller.resolve(null, null);
|
||||
fail("Expected exception");
|
||||
} catch (RuntimeException e) {
|
||||
assertTrue(e.getMessage().contains("不能同时为空"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_blankInput_shouldFail() {
|
||||
try {
|
||||
controller.resolve(" ", "");
|
||||
fail("Expected exception");
|
||||
} catch (RuntimeException e) {
|
||||
assertTrue(e.getMessage().contains("不能同时为空"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_notFound_shouldFail() {
|
||||
when(tenantCommonApi.getTenantByName("nonexistent")).thenReturn(null);
|
||||
when(tenantCommonApi.getTenantByWebsite(anyString())).thenReturn(null);
|
||||
|
||||
try {
|
||||
controller.resolve(null, "nonexistent");
|
||||
fail("Expected exception");
|
||||
} catch (RuntimeException e) {
|
||||
assertTrue(e.getMessage().contains("租户不存在"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_disabled_shouldFail() {
|
||||
TenantRespDTO dto = buildTenant(400L, "disabled-school", CommonStatusEnum.DISABLE.getStatus());
|
||||
when(tenantCommonApi.getTenantByName("disabled-school")).thenReturn(dto);
|
||||
|
||||
try {
|
||||
controller.resolve(null, "disabled-school");
|
||||
fail("Expected exception");
|
||||
} catch (RuntimeException e) {
|
||||
assertTrue(e.getMessage().contains("已被禁用"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_expired_shouldFail() {
|
||||
TenantRespDTO dto = buildTenant(500L, "expired-school", CommonStatusEnum.ENABLE.getStatus());
|
||||
dto.setExpireTime(LocalDateTime.now().minusDays(1));
|
||||
when(tenantCommonApi.getTenantByName("expired-school")).thenReturn(dto);
|
||||
|
||||
try {
|
||||
controller.resolve(null, "expired-school");
|
||||
fail("Expected exception");
|
||||
} catch (RuntimeException e) {
|
||||
assertTrue(e.getMessage().contains("当前租户不可用"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_protocolInHostname_shouldFail() {
|
||||
try {
|
||||
controller.resolve("http://evil.com", null);
|
||||
fail("Expected exception");
|
||||
} catch (RuntimeException e) {
|
||||
assertTrue(e.getMessage().contains("协议"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolve_pathInHostname_shouldFail() {
|
||||
try {
|
||||
controller.resolve("school.com/admin", null);
|
||||
fail("Expected exception");
|
||||
} catch (RuntimeException e) {
|
||||
assertTrue(e.getMessage().contains("路径"));
|
||||
}
|
||||
}
|
||||
|
||||
// ========== helpers ==========
|
||||
|
||||
private static TenantRespDTO buildTenant(Long id, String name, Integer status) {
|
||||
TenantRespDTO dto = new TenantRespDTO();
|
||||
dto.setId(id);
|
||||
dto.setName(name);
|
||||
dto.setStatus(status);
|
||||
dto.setWebsites(Collections.singletonList(name + ".example.com"));
|
||||
dto.setExpireTime(LocalDateTime.now().plusYears(1));
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
package cn.iocoder.yudao.module.education.framework.web.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi;
|
||||
import cn.iocoder.yudao.module.education.config.EducationProperties;
|
||||
import cn.iocoder.yudao.module.education.controller.admin.EducationCapabilityController;
|
||||
import cn.iocoder.yudao.module.education.controller.app.EducationContextController;
|
||||
import cn.iocoder.yudao.module.education.controller.app.tenant.EducationTenantController;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* {@link EducationWebConfiguration} 的单元测试
|
||||
@@ -14,12 +19,19 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
class EducationWebConfigurationTest {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withUserConfiguration(EducationWebConfiguration.class, EducationCapabilityController.class);
|
||||
.withUserConfiguration(EducationWebConfiguration.class,
|
||||
EducationCapabilityController.class,
|
||||
EducationTenantController.class,
|
||||
EducationContextController.class)
|
||||
.withBean(TenantCommonApi.class, () -> mock(TenantCommonApi.class))
|
||||
.withBean(EducationProperties.class, EducationProperties::new);
|
||||
|
||||
@Test
|
||||
void shouldNotRegisterEducationWebBeansByDefault() {
|
||||
contextRunner.run(context -> {
|
||||
assertThat(context).doesNotHaveBean(EducationCapabilityController.class);
|
||||
assertThat(context).doesNotHaveBean(EducationTenantController.class);
|
||||
assertThat(context).doesNotHaveBean(EducationContextController.class);
|
||||
assertThat(context).doesNotHaveBean("educationGroupedOpenApi");
|
||||
});
|
||||
}
|
||||
@@ -29,6 +41,8 @@ class EducationWebConfigurationTest {
|
||||
contextRunner.withPropertyValues("yudao.education.enabled=true")
|
||||
.run(context -> {
|
||||
assertThat(context).hasSingleBean(EducationCapabilityController.class);
|
||||
assertThat(context).hasSingleBean(EducationTenantController.class);
|
||||
assertThat(context).hasSingleBean(EducationContextController.class);
|
||||
assertThat(context).hasBean("educationGroupedOpenApi");
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user