218 lines
6.9 KiB
Go
218 lines
6.9 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/glebarez/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
|
|
"oci-portal/internal/model"
|
|
)
|
|
|
|
// newSystemLogEnv 建一个只含 SystemLog 表的内存库环境。
|
|
func newSystemLogEnv(t *testing.T) (*SystemLogService, *gorm.DB) {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("open in-memory sqlite: %v", err)
|
|
}
|
|
// :memory: 库每个连接彼此独立,Record 的异步 goroutine 可能拿到新连接而丢表,锁单连接
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
t.Fatalf("db handle: %v", err)
|
|
}
|
|
sqlDB.SetMaxOpenConns(1)
|
|
if err := db.AutoMigrate(&model.SystemLog{}); err != nil {
|
|
t.Fatalf("auto migrate: %v", err)
|
|
}
|
|
return NewSystemLogService(db), db
|
|
}
|
|
|
|
// seedLogs 由旧到新插入 n 条日志,时间逐分钟递增,路径 / 用户名带序号便于断言。
|
|
func seedLogs(t *testing.T, db *gorm.DB, n int) {
|
|
t.Helper()
|
|
base := time.Now().Add(-time.Duration(n) * time.Minute)
|
|
for i := 0; i < n; i++ {
|
|
row := model.SystemLog{
|
|
Username: fmt.Sprintf("user-%02d", i),
|
|
Method: "POST",
|
|
Path: fmt.Sprintf("/api/v1/res-%02d", i),
|
|
Status: 200,
|
|
CreatedAt: base.Add(time.Duration(i) * time.Minute),
|
|
}
|
|
if err := db.Create(&row).Error; err != nil {
|
|
t.Fatalf("seed log %d: %v", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSystemLogList(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
q SystemLogQuery
|
|
wantLen int
|
|
wantTotal int64
|
|
wantFirstPath string // 空串表示不校验首条
|
|
}{
|
|
{name: "零值分页取默认 20 条最新在前", q: SystemLogQuery{}, wantLen: 20, wantTotal: 25, wantFirstPath: "/api/v1/res-24"},
|
|
{name: "第二页取余下 5 条", q: SystemLogQuery{Page: 2, PageSize: 20}, wantLen: 5, wantTotal: 25, wantFirstPath: "/api/v1/res-04"},
|
|
{name: "pageSize 超上限被钳制仍能取全量", q: SystemLogQuery{PageSize: 200}, wantLen: 25, wantTotal: 25},
|
|
{name: "关键字模糊匹配路径", q: SystemLogQuery{Keyword: "res-1"}, wantLen: 10, wantTotal: 10, wantFirstPath: "/api/v1/res-19"},
|
|
{name: "关键字模糊匹配用户名", q: SystemLogQuery{Keyword: "user-05"}, wantLen: 1, wantTotal: 1, wantFirstPath: "/api/v1/res-05"},
|
|
{name: "关键字无匹配返回空页", q: SystemLogQuery{Keyword: "no-such"}, wantLen: 0, wantTotal: 0},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
svc, db := newSystemLogEnv(t)
|
|
seedLogs(t, db, 25)
|
|
items, total, err := svc.List(context.Background(), tt.q)
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
if len(items) != tt.wantLen || total != tt.wantTotal {
|
|
t.Errorf("got %d items total %d, want %d items total %d", len(items), total, tt.wantLen, tt.wantTotal)
|
|
}
|
|
if tt.wantFirstPath != "" && items[0].Path != tt.wantFirstPath {
|
|
t.Errorf("first path = %q, want %q", items[0].Path, tt.wantFirstPath)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSystemLogListStableOrder(t *testing.T) {
|
|
svc, db := newSystemLogEnv(t)
|
|
// 同一时间戳的两条记录按 id 倒序,保证翻页顺序稳定
|
|
ts := time.Now().Truncate(time.Second)
|
|
for i := 0; i < 2; i++ {
|
|
row := model.SystemLog{Path: fmt.Sprintf("/p%d", i), CreatedAt: ts}
|
|
if err := db.Create(&row).Error; err != nil {
|
|
t.Fatalf("seed: %v", err)
|
|
}
|
|
}
|
|
items, _, err := svc.List(context.Background(), SystemLogQuery{})
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
if len(items) != 2 || items[0].ID <= items[1].ID {
|
|
t.Errorf("order = %+v, want id desc within same created_at", items)
|
|
}
|
|
}
|
|
|
|
func TestSystemLogRecord(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
async bool
|
|
}{
|
|
{name: "record 同步落库", async: false},
|
|
{name: "Record 异步落库", async: true},
|
|
}
|
|
entry := model.SystemLog{
|
|
Username: "admin", Method: "PUT", Path: "/api/v1/oci-configs/:id",
|
|
Status: 200, DurationMs: 18, ClientIP: "203.0.113.7",
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
svc, db := newSystemLogEnv(t)
|
|
if tt.async {
|
|
svc.Record(entry)
|
|
svc.Wait()
|
|
} else if err := svc.record(context.Background(), entry); err != nil {
|
|
t.Fatalf("record: %v", err)
|
|
}
|
|
var got model.SystemLog
|
|
if err := db.First(&got).Error; err != nil {
|
|
t.Fatalf("load row: %v", err)
|
|
}
|
|
got.ID, got.CreatedAt = 0, entry.CreatedAt // 自动填充字段不参与对比
|
|
if got != entry {
|
|
t.Errorf("row = %+v, want %+v", got, entry)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSystemLogRecordFillsCreatedAt(t *testing.T) {
|
|
svc, db := newSystemLogEnv(t)
|
|
if err := svc.record(context.Background(), model.SystemLog{Path: "/p"}); err != nil {
|
|
t.Fatalf("record: %v", err)
|
|
}
|
|
var got model.SystemLog
|
|
if err := db.First(&got).Error; err != nil {
|
|
t.Fatalf("load row: %v", err)
|
|
}
|
|
if got.CreatedAt.IsZero() {
|
|
t.Error("createdAt is zero, want auto filled")
|
|
}
|
|
}
|
|
|
|
func TestSystemLogCleanup(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ages []time.Duration // 每条记录距今的时长,由旧到新
|
|
retention time.Duration
|
|
maxRows int
|
|
wantPaths []string // 清理后应保留的路径(按时间升序编号 p0..pN)
|
|
}{
|
|
{
|
|
name: "只删过期", ages: []time.Duration{100 * 24 * time.Hour, time.Hour},
|
|
retention: 90 * 24 * time.Hour, maxRows: 10, wantPaths: []string{"/p1"},
|
|
},
|
|
{
|
|
name: "超上限删最旧", ages: []time.Duration{5 * time.Hour, 4 * time.Hour, 3 * time.Hour, 2 * time.Hour, time.Hour},
|
|
retention: 90 * 24 * time.Hour, maxRows: 3, wantPaths: []string{"/p2", "/p3", "/p4"},
|
|
},
|
|
{
|
|
name: "都未触发保持原样", ages: []time.Duration{2 * time.Hour, time.Hour},
|
|
retention: 90 * 24 * time.Hour, maxRows: 10, wantPaths: []string{"/p0", "/p1"},
|
|
},
|
|
{
|
|
name: "过期与超量叠加", ages: []time.Duration{100 * 24 * time.Hour, 4 * time.Hour, 3 * time.Hour, 2 * time.Hour, time.Hour},
|
|
retention: 90 * 24 * time.Hour, maxRows: 2, wantPaths: []string{"/p3", "/p4"},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
svc, db := newSystemLogEnv(t)
|
|
for i, age := range tt.ages {
|
|
row := model.SystemLog{Path: fmt.Sprintf("/p%d", i), CreatedAt: time.Now().Add(-age)}
|
|
if err := db.Create(&row).Error; err != nil {
|
|
t.Fatalf("seed %d: %v", i, err)
|
|
}
|
|
}
|
|
if err := svc.cleanup(context.Background(), tt.retention, tt.maxRows); err != nil {
|
|
t.Fatalf("cleanup: %v", err)
|
|
}
|
|
var paths []string
|
|
if err := db.Model(&model.SystemLog{}).Order("created_at ASC").Pluck("path", &paths).Error; err != nil {
|
|
t.Fatalf("load rows: %v", err)
|
|
}
|
|
if fmt.Sprint(paths) != fmt.Sprint(tt.wantPaths) {
|
|
t.Errorf("kept = %v, want %v", paths, tt.wantPaths)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSystemLogStartCleanupStopsOnCancel(t *testing.T) {
|
|
svc, _ := newSystemLogEnv(t)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
svc.StartCleanup(ctx)
|
|
cancel()
|
|
done := make(chan struct{})
|
|
go func() {
|
|
svc.Wait()
|
|
close(done)
|
|
}()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("cleanup goroutine did not exit after context cancel")
|
|
}
|
|
}
|