Files
junhong_cmp_fiber/pkg/logger/rotation_test.go
huang eaa70ac255 feat: 实现 RBAC 权限系统和数据权限控制 (004-rbac-data-permission)
主要功能:
- 实现完整的 RBAC 权限系统(账号、角色、权限的多对多关联)
- 基于 owner_id + shop_id 的自动数据权限过滤
- 使用 PostgreSQL WITH RECURSIVE 查询下级账号
- Redis 缓存优化下级账号查询性能(30分钟过期)
- 支持多租户数据隔离和层级权限管理

技术实现:
- 新增 Account、Role、Permission 模型及关联关系表
- 实现 GORM Scopes 自动应用数据权限过滤
- 添加数据库迁移脚本(000002_rbac_data_permission、000003_add_owner_id_shop_id)
- 完善错误码定义(1010-1027 为 RBAC 相关错误)
- 重构 main.go 采用函数拆分提高可读性

测试覆盖:
- 添加 Account、Role、Permission 的集成测试
- 添加数据权限过滤的单元测试和集成测试
- 添加下级账号查询和缓存的单元测试
- 添加 API 回归测试确保向后兼容

文档更新:
- 更新 README.md 添加 RBAC 功能说明
- 更新 CLAUDE.md 添加技术栈和开发原则
- 添加 docs/004-rbac-data-permission/ 功能总结和使用指南

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 16:44:06 +08:00

389 lines
9.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package logger
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"go.uber.org/zap"
)
// TestLogRotation 测试日志轮转功能T027
func TestLogRotation(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
appLogFile := filepath.Join(tempDir, "app-rotation.log")
// 初始化日志系统,设置较小的 MaxSize 以便测试
err := InitLoggers("info", false,
LogRotationConfig{
Filename: appLogFile,
MaxSize: 1, // 1MB写入足够数据后会触发轮转
MaxBackups: 3,
MaxAge: 7,
Compress: false, // 不压缩以便检查
},
LogRotationConfig{
Filename: filepath.Join(tempDir, "access-rotation.log"),
MaxSize: 1,
MaxBackups: 3,
MaxAge: 7,
Compress: false,
},
)
if err != nil {
t.Fatalf("InitLoggers failed: %v", err)
}
logger := GetAppLogger()
// 写入大量日志数据以触发轮转每条约100字节写入15000条约1.5MB
largeMessage := strings.Repeat("a", 100)
for i := 0; i < 15000; i++ {
logger.Info(largeMessage,
zap.Int("iteration", i),
zap.String("data", largeMessage),
)
}
// 刷新缓冲区
if err := Sync(); err != nil {
t.Fatalf("Sync failed: %v", err)
}
// 等待一小段时间确保文件写入完成
time.Sleep(100 * time.Millisecond)
// 验证主日志文件存在
if _, err := os.Stat(appLogFile); os.IsNotExist(err) {
t.Error("Main log file should exist")
}
// 检查是否有备份文件(轮转后的文件)
files, err := filepath.Glob(filepath.Join(tempDir, "app-rotation-*.log"))
if err != nil {
t.Fatalf("Failed to glob backup files: %v", err)
}
// 由于写入了超过1MB的数据应该触发至少一次轮转
if len(files) == 0 {
// 可能系统写入速度或lumberjack行为导致未立即轮转检查主文件大小
info, err := os.Stat(appLogFile)
if err != nil {
t.Fatalf("Failed to stat main log file: %v", err)
}
if info.Size() == 0 {
t.Error("Log file should have content")
}
// 不强制要求必须轮转,因为取决于具体实现
t.Logf("No rotation occurred, but main log file size: %d bytes", info.Size())
} else {
t.Logf("Found %d rotated backup file(s)", len(files))
}
}
// TestMaxBackups 测试最大备份数限制T027
func TestMaxBackups(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
appLogFile := filepath.Join(tempDir, "app-backups.log")
// 初始化日志系统,设置 MaxBackups=2
err := InitLoggers("info", false,
LogRotationConfig{
Filename: appLogFile,
MaxSize: 1, // 1MB
MaxBackups: 2, // 最多保留2个备份
MaxAge: 7,
Compress: false,
},
LogRotationConfig{
Filename: filepath.Join(tempDir, "access-backups.log"),
MaxSize: 1,
MaxBackups: 2,
MaxAge: 7,
Compress: false,
},
)
if err != nil {
t.Fatalf("InitLoggers failed: %v", err)
}
logger := GetAppLogger()
// 写入足够的数据触发多次轮转每次1.5MB共4.5MB应该触发3次轮转
largeMessage := strings.Repeat("b", 100)
for round := 0; round < 3; round++ {
for i := 0; i < 15000; i++ {
logger.Info(largeMessage,
zap.Int("round", round),
zap.Int("iteration", i),
)
}
_ = Sync()
time.Sleep(100 * time.Millisecond)
}
// 等待轮转完成
time.Sleep(200 * time.Millisecond)
// 检查备份文件数量
files, err := filepath.Glob(filepath.Join(tempDir, "app-backups-*.log"))
if err != nil {
t.Fatalf("Failed to glob backup files: %v", err)
}
// 由于 MaxBackups=2即使触发了多次轮转也只应保留最多2个备份文件
// (实际行为取决于 lumberjack 的实现细节可能小于等于2
if len(files) > 2 {
t.Errorf("Expected at most 2 backup files due to MaxBackups=2, got %d", len(files))
}
t.Logf("Found %d backup file(s) with MaxBackups=2", len(files))
}
// TestCompressionConfig 测试压缩配置T027
func TestCompressionConfig(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
tests := []struct {
name string
compress bool
}{
{
name: "compression enabled",
compress: true,
},
{
name: "compression disabled",
compress: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logFile := filepath.Join(tempDir, "app-"+tt.name+".log")
err := InitLoggers("info", false,
LogRotationConfig{
Filename: logFile,
MaxSize: 1,
MaxBackups: 3,
MaxAge: 7,
Compress: tt.compress,
},
LogRotationConfig{
Filename: filepath.Join(tempDir, "access-"+tt.name+".log"),
MaxSize: 1,
MaxBackups: 3,
MaxAge: 7,
Compress: tt.compress,
},
)
if err != nil {
t.Fatalf("InitLoggers failed: %v", err)
}
logger := GetAppLogger()
// 写入一些日志
for i := 0; i < 1000; i++ {
logger.Info("test compression",
zap.Int("id", i),
zap.String("data", strings.Repeat("c", 50)),
)
}
_ = Sync()
time.Sleep(100 * time.Millisecond)
// 验证日志文件存在
if _, err := os.Stat(logFile); os.IsNotExist(err) {
t.Error("Log file should exist")
}
})
}
}
// TestMaxAge 测试日志文件保留时间T027
func TestMaxAge(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
// 初始化日志系统,设置 MaxAge=1 天
err := InitLoggers("info", false,
LogRotationConfig{
Filename: filepath.Join(tempDir, "app-maxage.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 1, // 1天
Compress: false,
},
LogRotationConfig{
Filename: filepath.Join(tempDir, "access-maxage.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 1,
Compress: false,
},
)
if err != nil {
t.Fatalf("InitLoggers failed: %v", err)
}
logger := GetAppLogger()
// 写入日志
logger.Info("test max age", zap.String("config", "maxage=1"))
_ = Sync()
// 验证配置已应用无法在单元测试中验证实际的清理行为因为需要等待1天
// 这里只验证初始化没有错误
if logger == nil {
t.Error("Logger should be initialized with MaxAge config")
}
}
// TestNewLumberjackLogger 测试 Lumberjack logger 创建T027
func TestNewLumberjackLogger(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
tests := []struct {
name string
config LogRotationConfig
}{
{
name: "standard config",
config: LogRotationConfig{
Filename: filepath.Join(tempDir, "test1.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
},
{
name: "minimal config",
config: LogRotationConfig{
Filename: filepath.Join(tempDir, "test2.log"),
MaxSize: 1,
MaxBackups: 1,
MaxAge: 1,
Compress: false,
},
},
{
name: "large config",
config: LogRotationConfig{
Filename: filepath.Join(tempDir, "test3.log"),
MaxSize: 100,
MaxBackups: 10,
MaxAge: 30,
Compress: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger := newLumberjackLogger(tt.config)
if logger == nil {
t.Error("newLumberjackLogger should not return nil")
}
// 验证配置已正确设置
if logger.Filename != tt.config.Filename {
t.Errorf("Filename = %v, want %v", logger.Filename, tt.config.Filename)
}
if logger.MaxSize != tt.config.MaxSize {
t.Errorf("MaxSize = %v, want %v", logger.MaxSize, tt.config.MaxSize)
}
if logger.MaxBackups != tt.config.MaxBackups {
t.Errorf("MaxBackups = %v, want %v", logger.MaxBackups, tt.config.MaxBackups)
}
if logger.MaxAge != tt.config.MaxAge {
t.Errorf("MaxAge = %v, want %v", logger.MaxAge, tt.config.MaxAge)
}
if logger.Compress != tt.config.Compress {
t.Errorf("Compress = %v, want %v", logger.Compress, tt.config.Compress)
}
if !logger.LocalTime {
t.Error("LocalTime should be true")
}
})
}
}
// TestConcurrentLogging 测试并发日志写入T027
func TestConcurrentLogging(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
// 初始化日志系统
err := InitLoggers("info", false,
LogRotationConfig{
Filename: filepath.Join(tempDir, "app-concurrent.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: false,
},
LogRotationConfig{
Filename: filepath.Join(tempDir, "access-concurrent.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: false,
},
)
if err != nil {
t.Fatalf("InitLoggers failed: %v", err)
}
logger := GetAppLogger()
// 启动多个 goroutine 并发写入日志
done := make(chan bool)
goroutines := 10
messagesPerGoroutine := 100
for i := 0; i < goroutines; i++ {
go func(id int) {
for j := 0; j < messagesPerGoroutine; j++ {
logger.Info("concurrent log message",
zap.Int("goroutine", id),
zap.Int("message", j),
)
}
done <- true
}(i)
}
// 等待所有 goroutine 完成
for i := 0; i < goroutines; i++ {
<-done
}
// 刷新缓冲区
if err := Sync(); err != nil {
t.Fatalf("Sync failed: %v", err)
}
// 验证日志文件存在且有内容
logFile := filepath.Join(tempDir, "app-concurrent.log")
info, err := os.Stat(logFile)
if err != nil {
t.Fatalf("Failed to stat log file: %v", err)
}
if info.Size() == 0 {
t.Error("Log file should have content after concurrent writes")
}
t.Logf("Concurrent logging test completed, log file size: %d bytes", info.Size())
}