实现系统管理后台 V1
This commit is contained in:
@@ -81,6 +81,7 @@ class AuthControllerTest {
|
||||
"SYSTEM_ROLE_MANAGE",
|
||||
"SYSTEM_MENU_MANAGE",
|
||||
"HOTEL_MANAGE",
|
||||
"SYSTEM_ADMIN_CONSOLE_ACCESS",
|
||||
"SYSTEM_DEBUG_EML_RUN")))
|
||||
.andExpect(jsonPath("$.menus[0].menu_code").value("RESERVATION_ORDERS"))
|
||||
.andExpect(jsonPath("$.menus[0].route_path").value("/reservation/orders"))
|
||||
@@ -91,6 +92,9 @@ class AuthControllerTest {
|
||||
.andExpect(jsonPath("$.menus[2].menu_code").value("DEBUG_EML_SUPERAGENT"))
|
||||
.andExpect(jsonPath("$.menus[2].route_path").value("/debug/eml-superagent"))
|
||||
.andExpect(jsonPath("$.menus[2].icon_key").value("pi pi-upload"))
|
||||
.andExpect(jsonPath("$.menus[3].menu_code").value("SYSTEM_SETTINGS"))
|
||||
.andExpect(jsonPath("$.menus[3].route_path").value("/system"))
|
||||
.andExpect(jsonPath("$.menus[3].icon_key").value("pi pi-cog"))
|
||||
.andReturn();
|
||||
|
||||
String token = tokenFrom(loginResult);
|
||||
@@ -100,7 +104,7 @@ class AuthControllerTest {
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.user.username").value("m003-admin"))
|
||||
.andExpect(jsonPath("$.default_hotel_id").value("HOTEL-TEST"))
|
||||
.andExpect(jsonPath("$.menus[2].menu_code").value("DEBUG_EML_SUPERAGENT"));
|
||||
.andExpect(jsonPath("$.menus[3].menu_code").value("SYSTEM_SETTINGS"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
package cn.nianxx.thhotel.platform.system.control;
|
||||
|
||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import cn.nianxx.thhotel.ThHotelApplication;
|
||||
import cn.nianxx.thhotel.platform.access.common.enums.PlatformRoleCode;
|
||||
import cn.nianxx.thhotel.platform.access.domain.PlatformPermissionEntity;
|
||||
import cn.nianxx.thhotel.platform.access.domain.PlatformRoleEntity;
|
||||
import cn.nianxx.thhotel.platform.access.repository.PlatformAccessRepository;
|
||||
import cn.nianxx.thhotel.platform.hotel.repository.PlatformHotelRepository;
|
||||
import cn.nianxx.thhotel.platform.identity.common.enums.PlatformUserStatus;
|
||||
import cn.nianxx.thhotel.platform.identity.domain.PlatformUserEntity;
|
||||
import cn.nianxx.thhotel.platform.identity.repository.PlatformIdentityRepository;
|
||||
import cn.nianxx.thhotel.platform.identity.service.impl.AuthPasswordService;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.time.LocalDateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
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.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
@SpringBootTest(
|
||||
classes = ThHotelApplication.class,
|
||||
properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:m006_admin_readonly;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
|
||||
"auth.bootstrap.admin.username=m006-admin",
|
||||
"auth.bootstrap.admin.password=Admin@123456",
|
||||
"auth.bootstrap.admin.display-name=系统管理员",
|
||||
"auth.bootstrap.default-hotel-id=HOTEL-TEST",
|
||||
"auth.bootstrap.default-hotel-name=测试酒店",
|
||||
"auth.bootstrap.default-hotel-time-zone=Asia/Bangkok"
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class AdminReadonlyControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
@Autowired
|
||||
private PlatformIdentityRepository identityRepository;
|
||||
@Autowired
|
||||
private PlatformAccessRepository accessRepository;
|
||||
@Autowired
|
||||
private PlatformHotelRepository hotelRepository;
|
||||
@Autowired
|
||||
private AuthPasswordService passwordService;
|
||||
|
||||
@BeforeEach
|
||||
void ensureViewerUser() {
|
||||
PlatformUserEntity user = identityRepository.findUserByUsername("m006-viewer")
|
||||
.orElseGet(() -> {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
PlatformUserEntity created = new PlatformUserEntity();
|
||||
created.setUsername("m006-viewer");
|
||||
created.setPasswordHash(passwordService.hash("Viewer@123456"));
|
||||
created.setDisplayName("只读用户");
|
||||
created.setUserStatus(PlatformUserStatus.ACTIVE.name());
|
||||
created.setSuperAdmin(false);
|
||||
created.setPasswordChangedAt(now);
|
||||
created.setCreatedAt(now);
|
||||
created.setUpdatedAt(now);
|
||||
identityRepository.insertUser(created);
|
||||
return created;
|
||||
});
|
||||
PlatformRoleEntity viewerRole = accessRepository.findRoleByCode(PlatformRoleCode.RESERVATION_VIEWER.name())
|
||||
.orElseThrow();
|
||||
accessRepository.ensureUserRole(user.getId(), viewerRole.getId());
|
||||
hotelRepository.ensureUserHotel(user.getId(), "HOTEL-TEST", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectAdminEndpointWhenTokenMissing() throws Exception {
|
||||
mockMvc.perform(get("/api/admin/users")
|
||||
.param("page_num", "1")
|
||||
.param("page_size", "20"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.error_code").value("ADMIN_AUTH_REQUIRED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectAdminEndpointWhenTokenInvalid() throws Exception {
|
||||
mockMvc.perform(get("/api/admin/users")
|
||||
.header("Authorization", "Bearer invalid-token")
|
||||
.param("page_num", "1")
|
||||
.param("page_size", "20"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.error_code").value("AUTH_SESSION_INVALID"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowSystemAdminToReadAdminLists() throws Exception {
|
||||
String token = tokenFrom(login("m006-admin", "Admin@123456"));
|
||||
|
||||
mockMvc.perform(get("/api/admin/users")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.param("page_num", "1")
|
||||
.param("page_size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items.length()", greaterThanOrEqualTo(1)))
|
||||
.andExpect(jsonPath("$.page.page_num").value(1))
|
||||
.andExpect(jsonPath("$.page.page_size").value(20));
|
||||
|
||||
mockMvc.perform(get("/api/admin/roles")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.param("page_num", "1")
|
||||
.param("page_size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items[0].role_code").value(notNullValue()));
|
||||
|
||||
mockMvc.perform(get("/api/admin/permissions")
|
||||
.header("Authorization", "Bearer " + token))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].permission_code").value(notNullValue()));
|
||||
|
||||
mockMvc.perform(get("/api/admin/menus")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.param("page_num", "1")
|
||||
.param("page_size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items[?(@.menu_code=='SYSTEM_SETTINGS')]").exists());
|
||||
|
||||
mockMvc.perform(get("/api/admin/hotels")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.param("page_num", "1")
|
||||
.param("page_size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items[0].hotel_id").value("HOTEL-TEST"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectAdminEndpointWhenPermissionMissing() throws Exception {
|
||||
String token = tokenFrom(login("m006-viewer", "Viewer@123456"));
|
||||
|
||||
mockMvc.perform(get("/api/admin/users")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.param("page_num", "1")
|
||||
.param("page_size", "20"))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("ADMIN_PERMISSION_DENIED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateUserAssignHotelsResetPasswordAndRevokeSessionWhenDisabled() throws Exception {
|
||||
String token = tokenFrom(login("m006-admin", "Admin@123456"));
|
||||
PlatformRoleEntity viewerRole = accessRepository.findRoleByCode(PlatformRoleCode.RESERVATION_VIEWER.name())
|
||||
.orElseThrow();
|
||||
String username = "m006-user-" + System.nanoTime();
|
||||
|
||||
MvcResult createResult = mockMvc.perform(post("/api/admin/users")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"username": "%s",
|
||||
"initial_password": "User@123456",
|
||||
"display_name": "测试用户",
|
||||
"user_status": "ACTIVE",
|
||||
"role_ids": ["%s"],
|
||||
"hotel_ids": ["HOTEL-TEST"],
|
||||
"default_hotel_id": "HOTEL-TEST"
|
||||
}
|
||||
""".formatted(username, viewerRole.getId())))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.username").value(username))
|
||||
.andExpect(jsonPath("$.roles[0].role_code").value(PlatformRoleCode.RESERVATION_VIEWER.name()))
|
||||
.andExpect(jsonPath("$.hotels[0].hotel_id").value("HOTEL-TEST"))
|
||||
.andReturn();
|
||||
String userId = objectMapper.readTree(createResult.getResponse().getContentAsString()).path("id").asText();
|
||||
String userToken = tokenFrom(login(username, "User@123456"));
|
||||
|
||||
mockMvc.perform(post("/api/admin/users/{userId}/password-reset", userId)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.user_id").value(userId))
|
||||
.andExpect(jsonPath("$.temporary_password").value(notNullValue()));
|
||||
|
||||
mockMvc.perform(put("/api/admin/users/{userId}/roles", userId)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"role_ids": []
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.roles.length()").value(0));
|
||||
|
||||
mockMvc.perform(put("/api/admin/users/{userId}/hotels", userId)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"hotel_ids": ["HOTEL-TEST"],
|
||||
"default_hotel_id": "HOTEL-TEST"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.hotels[0].default_hotel").value(true));
|
||||
|
||||
String disabledHotelId = "M006-DISABLED-" + System.nanoTime();
|
||||
mockMvc.perform(post("/api/admin/hotels")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"hotel_id": "%s",
|
||||
"hotel_name": "M006 禁用酒店",
|
||||
"time_zone": "Asia/Bangkok",
|
||||
"sort_order": 199
|
||||
}
|
||||
""".formatted(disabledHotelId)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.hotel_status").value("DISABLED"));
|
||||
|
||||
mockMvc.perform(put("/api/admin/users/{userId}/hotels", userId)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"hotel_ids": ["HOTEL-TEST", "%s"],
|
||||
"default_hotel_id": "HOTEL-TEST"
|
||||
}
|
||||
""".formatted(disabledHotelId)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("ADMIN_INVALID_REQUEST"));
|
||||
|
||||
mockMvc.perform(put("/api/admin/users/{userId}", userId)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"display_name": "测试用户已禁用",
|
||||
"user_status": "DISABLED"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.user_status").value("DISABLED"));
|
||||
|
||||
mockMvc.perform(get("/api/admin/audits")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.param("target_type", "PLATFORM_USER")
|
||||
.param("target_id", userId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items[0].target_type").value("PLATFORM_USER"));
|
||||
|
||||
mockMvc.perform(get("/api/auth/me")
|
||||
.header("Authorization", "Bearer " + userToken))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldManageCustomRoleAndRejectBuiltinRoleMutation() throws Exception {
|
||||
String token = tokenFrom(login("m006-admin", "Admin@123456"));
|
||||
String roleCode = "M006_CUSTOM_" + System.nanoTime();
|
||||
PlatformPermissionEntity permission = accessRepository.findPermissionByCode("SYSTEM_USER_MANAGE").orElseThrow();
|
||||
|
||||
MvcResult createResult = mockMvc.perform(post("/api/admin/roles")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"role_code": "%s",
|
||||
"role_name": "M006 自定义角色",
|
||||
"role_status": "ACTIVE"
|
||||
}
|
||||
""".formatted(roleCode)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.role_code").value(roleCode))
|
||||
.andExpect(jsonPath("$.system_builtin").value(false))
|
||||
.andReturn();
|
||||
String roleId = objectMapper.readTree(createResult.getResponse().getContentAsString()).path("id").asText();
|
||||
|
||||
mockMvc.perform(put("/api/admin/roles/{roleId}/permissions", roleId)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"permission_ids": ["%s"]
|
||||
}
|
||||
""".formatted(permission.getId())))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.permissions[0].permission_code").value("SYSTEM_USER_MANAGE"));
|
||||
|
||||
PlatformRoleEntity builtinRole = accessRepository.findRoleByCode(PlatformRoleCode.SYSTEM_ADMIN.name())
|
||||
.orElseThrow();
|
||||
mockMvc.perform(put("/api/admin/roles/{roleId}", builtinRole.getId().toString())
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"role_name": "不允许修改",
|
||||
"role_status": "ACTIVE"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.error_code").value("ADMIN_CONFLICT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateUnknownMenuAndKeepKnownRouteFlagFalse() throws Exception {
|
||||
String token = tokenFrom(login("m006-admin", "Admin@123456"));
|
||||
String menuCode = "M006_MENU_" + System.nanoTime();
|
||||
|
||||
MvcResult createResult = mockMvc.perform(post("/api/admin/menus")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"menu_code": "%s",
|
||||
"menu_name": "未来菜单",
|
||||
"menu_type": "PAGE",
|
||||
"route_path": "/future/not-ready",
|
||||
"permission_code": "SYSTEM_USER_MANAGE",
|
||||
"visible": true,
|
||||
"menu_status": "ACTIVE"
|
||||
}
|
||||
""".formatted(menuCode)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.menu_code").value(menuCode))
|
||||
.andExpect(jsonPath("$.known_route").value(false))
|
||||
.andReturn();
|
||||
String menuId = objectMapper.readTree(createResult.getResponse().getContentAsString()).path("id").asText();
|
||||
|
||||
mockMvc.perform(put("/api/admin/menus/{menuId}", menuId)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"menu_name": "未来菜单更新",
|
||||
"menu_type": "PAGE",
|
||||
"route_path": "/system/users",
|
||||
"permission_code": "SYSTEM_USER_MANAGE",
|
||||
"visible": true,
|
||||
"menu_status": "ACTIVE"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.menu_code").value(menuCode))
|
||||
.andExpect(jsonPath("$.known_route").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateDisabledHotelAndRejectBreakingSingleActiveHotelRule() throws Exception {
|
||||
String token = tokenFrom(login("m006-admin", "Admin@123456"));
|
||||
String hotelId = "M006-HOTEL-" + System.nanoTime();
|
||||
|
||||
mockMvc.perform(post("/api/admin/hotels")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"hotel_id": "%s",
|
||||
"hotel_name": "M006 测试酒店",
|
||||
"time_zone": "Asia/Bangkok",
|
||||
"sort_order": 99
|
||||
}
|
||||
""".formatted(hotelId)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.hotel_id").value(hotelId))
|
||||
.andExpect(jsonPath("$.hotel_status").value("DISABLED"));
|
||||
|
||||
mockMvc.perform(put("/api/admin/hotels/{hotelId}/status", hotelId)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"hotel_status": "ACTIVE"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.error_code").value("ADMIN_CONFLICT"));
|
||||
|
||||
mockMvc.perform(put("/api/admin/hotels/{hotelId}/status", "HOTEL-TEST")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"hotel_status": "DISABLED"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.error_code").value("ADMIN_CONFLICT"));
|
||||
}
|
||||
|
||||
private MvcResult login(String username, String password) throws Exception {
|
||||
return mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"username": "%s",
|
||||
"password": "%s"
|
||||
}
|
||||
""".formatted(username, password)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
private String tokenFrom(MvcResult result) throws Exception {
|
||||
JsonNode json = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||
return json.path("access_token").asText();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user