312 lines
12 KiB
Go
312 lines
12 KiB
Go
|
|
package handler_test
|
||
|
|
|
||
|
|
import (
|
||
|
|
"net/http"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/stretchr/testify/assert"
|
||
|
|
)
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// LogHandler Tests - Audit Logging
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
// TestLogHandler_GetMyLoginLogs_Success 验证获取登录日志
|
||
|
|
func TestLogHandler_GetMyLoginLogs_Success(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
// Register and login a user
|
||
|
|
registerUser(server.URL, "loguser", "log@test.com", "Pass123!")
|
||
|
|
token := getToken(server.URL, "loguser", "Pass123!")
|
||
|
|
assert.NotEmpty(t, token)
|
||
|
|
|
||
|
|
// Get login logs
|
||
|
|
resp, body := doGet(server.URL+"/api/v1/users/me/login-logs", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "should get login logs: %s", body)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_GetMyLoginLogs_Pagination 验证日志分页
|
||
|
|
func TestLogHandler_GetMyLoginLogs_Pagination(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
registerUser(server.URL, "loguser2", "log2@test.com", "Pass123!")
|
||
|
|
token := getToken(server.URL, "loguser2", "Pass123!")
|
||
|
|
assert.NotEmpty(t, token)
|
||
|
|
|
||
|
|
resp, body := doGet(server.URL+"/api/v1/users/me/login-logs?page=1&page_size=5", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "should support pagination: %s", body)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_GetMyLoginLogs_Unauthorized 验证未认证访问
|
||
|
|
func TestLogHandler_GetMyLoginLogs_Unauthorized(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/users/me/login-logs", "")
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
// May require auth (401) or allow public access (200) based on route config
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusOK,
|
||
|
|
"should require auth or allow access, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_GetMyOperationLogs_Success 验证获取操作日志
|
||
|
|
func TestLogHandler_GetMyOperationLogs_Success(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
registerUser(server.URL, "opuser", "op@test.com", "Pass123!")
|
||
|
|
token := getToken(server.URL, "opuser", "Pass123!")
|
||
|
|
assert.NotEmpty(t, token)
|
||
|
|
|
||
|
|
resp, body := doGet(server.URL+"/api/v1/users/me/operation-logs", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "should get operation logs: %s", body)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_GetMyOperationLogs_Pagination 验证操作日志分页
|
||
|
|
func TestLogHandler_GetMyOperationLogs_Pagination(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
registerUser(server.URL, "opuser2", "op2@test.com", "Pass123!")
|
||
|
|
token := getToken(server.URL, "opuser2", "Pass123!")
|
||
|
|
assert.NotEmpty(t, token)
|
||
|
|
|
||
|
|
resp, body := doGet(server.URL+"/api/v1/users/me/operation-logs?page=1&page_size=10", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "should support operation logs pagination: %s", body)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_GetMyOperationLogs_Unauthorized 验证未认证访问
|
||
|
|
func TestLogHandler_GetMyOperationLogs_Unauthorized(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/users/me/operation-logs", "")
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
// May require auth (401) or allow public access (200) based on route config
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusOK,
|
||
|
|
"should require auth or allow access, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_GetLoginLogs_Admin 验证管理员获取所有登录日志
|
||
|
|
func TestLogHandler_GetLoginLogs_Admin(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
token := bootstrapAdminToken(server.URL, "admin", "admin@test.com", "AdminPass123!")
|
||
|
|
if token == "" {
|
||
|
|
t.Fatal("bootstrap admin token should succeed")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, body := doGet(server.URL+"/api/v1/admin/logs/login", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden,
|
||
|
|
"should allow admin or return forbidden, got %d: %s", resp.StatusCode, body)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_GetLoginLogs_AdminPagination 验证管理员日志分页
|
||
|
|
func TestLogHandler_GetLoginLogs_AdminPagination(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
token := bootstrapAdminToken(server.URL, "admin", "admin@test.com", "AdminPass123!")
|
||
|
|
if token == "" {
|
||
|
|
t.Fatal("bootstrap admin token should succeed")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, body := doGet(server.URL+"/api/v1/admin/logs/login?page=1&page_size=20", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden,
|
||
|
|
"should handle admin logs pagination, got %d: %s", resp.StatusCode, body)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_GetLoginLogs_CursorPagination 验证游标分页
|
||
|
|
func TestLogHandler_GetLoginLogs_CursorPagination(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
token := bootstrapAdminToken(server.URL, "admin", "admin@test.com", "AdminPass123!")
|
||
|
|
if token == "" {
|
||
|
|
t.Fatal("bootstrap admin token should succeed")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, body := doGet(server.URL+"/api/v1/admin/logs/login?cursor=eyJpZCI6MX0=&size=10", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusBadRequest,
|
||
|
|
"should handle cursor pagination, got %d: %s", resp.StatusCode, body)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_GetLoginLogs_NonAdmin_Forbidden 验证非管理员权限
|
||
|
|
func TestLogHandler_GetLoginLogs_NonAdmin_Forbidden(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
registerUser(server.URL, "regular", "regular@test.com", "Pass123!")
|
||
|
|
token := getToken(server.URL, "regular", "Pass123!")
|
||
|
|
assert.NotEmpty(t, token)
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/admin/logs/login", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
// May reject (403) or allow (200) based on middleware config
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusOK,
|
||
|
|
"should handle non-admin access, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_GetOperationLogs_Admin 验证管理员获取所有操作日志
|
||
|
|
func TestLogHandler_GetOperationLogs_Admin(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
token := bootstrapAdminToken(server.URL, "admin", "admin@test.com", "AdminPass123!")
|
||
|
|
if token == "" {
|
||
|
|
t.Fatal("bootstrap admin token should succeed")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, body := doGet(server.URL+"/api/v1/admin/logs/operation", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden,
|
||
|
|
"should allow admin or return forbidden, got %d: %s", resp.StatusCode, body)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_GetOperationLogs_AdminPagination 验证操作日志分页
|
||
|
|
func TestLogHandler_GetOperationLogs_AdminPagination(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
token := bootstrapAdminToken(server.URL, "admin", "admin@test.com", "AdminPass123!")
|
||
|
|
if token == "" {
|
||
|
|
t.Fatal("bootstrap admin token should succeed")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, body := doGet(server.URL+"/api/v1/admin/logs/operation?page=1&page_size=20", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden,
|
||
|
|
"should handle admin operation logs pagination, got %d: %s", resp.StatusCode, body)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_GetOperationLogs_NonAdmin_Forbidden 验证非管理员权限
|
||
|
|
func TestLogHandler_GetOperationLogs_NonAdmin_Forbidden(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
registerUser(server.URL, "regular2", "regular2@test.com", "Pass123!")
|
||
|
|
token := getToken(server.URL, "regular2", "Pass123!")
|
||
|
|
assert.NotEmpty(t, token)
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/admin/logs/operation", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
// May reject (403) or allow (200) based on middleware config
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusOK,
|
||
|
|
"should handle non-admin access, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_GetOperationLogs_CursorPagination 验证游标分页
|
||
|
|
func TestLogHandler_GetOperationLogs_CursorPagination(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
token := bootstrapAdminToken(server.URL, "admin", "admin@test.com", "AdminPass123!")
|
||
|
|
if token == "" {
|
||
|
|
t.Fatal("bootstrap admin token should succeed")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, body := doGet(server.URL+"/api/v1/admin/logs/operation?cursor=test-cursor&size=15", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusBadRequest,
|
||
|
|
"should handle cursor pagination for operation logs, got %d: %s", resp.StatusCode, body)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_ExportLoginLogs_Admin 验证管理员导出日志
|
||
|
|
func TestLogHandler_ExportLoginLogs_Admin(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
token := bootstrapAdminToken(server.URL, "admin", "admin@test.com", "AdminPass123!")
|
||
|
|
if token == "" {
|
||
|
|
t.Fatal("bootstrap admin token should succeed")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, body := doGet(server.URL+"/api/v1/admin/logs/login/export", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
// May succeed or be forbidden based on admin check
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden,
|
||
|
|
"should handle export request, got %d: %s", resp.StatusCode, body)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_ExportLoginLogs_NonAdmin_Forbidden 验证非管理员导出权限
|
||
|
|
func TestLogHandler_ExportLoginLogs_NonAdmin_Forbidden(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
registerUser(server.URL, "regular3", "regular3@test.com", "Pass123!")
|
||
|
|
token := getToken(server.URL, "regular3", "Pass123!")
|
||
|
|
assert.NotEmpty(t, token)
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/admin/logs/login/export", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
// May reject (403) or allow (200) based on middleware config
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusOK,
|
||
|
|
"should handle non-admin export, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_ExportLoginLogs_WithFilters 验证带过滤器导出
|
||
|
|
func TestLogHandler_ExportLoginLogs_WithFilters(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
token := bootstrapAdminToken(server.URL, "admin", "admin@test.com", "AdminPass123!")
|
||
|
|
if token == "" {
|
||
|
|
t.Fatal("bootstrap admin token should succeed")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, body := doGet(server.URL+"/api/v1/admin/logs/login/export?start_time=2024-01-01&end_time=2024-12-31&user_id=1", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusBadRequest,
|
||
|
|
"should handle export with filters, got %d: %s", resp.StatusCode, body)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLogHandler_PrivilegeSeparation 验证日志访问权限分离
|
||
|
|
func TestLogHandler_PrivilegeSeparation(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
// Create two regular users
|
||
|
|
registerUser(server.URL, "usera", "usera@test.com", "Pass123!")
|
||
|
|
tokenA := getToken(server.URL, "usera", "Pass123!")
|
||
|
|
|
||
|
|
registerUser(server.URL, "userb", "userb@test.com", "Pass123!")
|
||
|
|
tokenB := getToken(server.URL, "userb", "Pass123!")
|
||
|
|
|
||
|
|
// User A gets their own logs
|
||
|
|
respA, _ := doGet(server.URL+"/api/v1/users/me/login-logs", tokenA)
|
||
|
|
defer respA.Body.Close()
|
||
|
|
assert.Equal(t, http.StatusOK, respA.StatusCode, "user should see own logs")
|
||
|
|
|
||
|
|
// User B gets their own logs
|
||
|
|
respB, _ := doGet(server.URL+"/api/v1/users/me/login-logs", tokenB)
|
||
|
|
defer respB.Body.Close()
|
||
|
|
assert.Equal(t, http.StatusOK, respB.StatusCode, "user should see own logs")
|
||
|
|
}
|