300 lines
9.0 KiB
Go
300 lines
9.0 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"ws_usernode/internal/model"
|
|
)
|
|
|
|
func TestSettingServiceCRUD(t *testing.T) {
|
|
db := testDB(t)
|
|
cfg := testConfig()
|
|
svc := NewSettingService(db, cfg)
|
|
ctx := context.Background()
|
|
|
|
// 初始值 = config 默认
|
|
items, err := svc.GetAll(ctx)
|
|
if err != nil {
|
|
t.Fatalf("getall: %v", err)
|
|
}
|
|
if len(items) != 3 {
|
|
t.Fatalf("items = %d, want 3", len(items))
|
|
}
|
|
for _, it := range items {
|
|
if it.Overridden {
|
|
t.Fatalf("initial item %s should not be overridden", it.Key)
|
|
}
|
|
}
|
|
if d := svc.DefaultTTL(ctx); d != cfg.Policy.DefaultTTL {
|
|
t.Fatalf("default ttl = %s, want %s", d, cfg.Policy.DefaultTTL)
|
|
}
|
|
|
|
// 更新
|
|
if err := svc.Set(ctx, "policy.default_ttl", "720h"); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
if d := svc.DefaultTTL(ctx); d != 720*time.Hour {
|
|
t.Fatalf("default ttl after set = %s, want 720h", d)
|
|
}
|
|
items, _ = svc.GetAll(ctx)
|
|
for _, it := range items {
|
|
if it.Key == "policy.default_ttl" {
|
|
if !it.Overridden || it.Value != "720h" {
|
|
t.Fatalf("item = %+v, want overridden 720h", it)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 未知 key / 非法时长
|
|
if err := svc.Set(ctx, "smtp.host", "x"); !errors.Is(err, ErrSettingKeyUnknown) {
|
|
t.Fatalf("unknown key err = %v, want ErrSettingKeyUnknown", err)
|
|
}
|
|
if err := svc.Set(ctx, "policy.default_ttl", "not-a-duration"); !errors.Is(err, ErrSettingValueInvalid) {
|
|
t.Fatalf("bad value err = %v, want ErrSettingValueInvalid", err)
|
|
}
|
|
}
|
|
|
|
func TestSettingServiceRecycleRetention(t *testing.T) {
|
|
db := testDB(t)
|
|
cfg := testConfig()
|
|
svc := NewSettingService(db, cfg)
|
|
ctx := context.Background()
|
|
// 未覆盖时回退 config
|
|
if d := svc.RecyclePeriod(ctx); d != cfg.Policy.RecyclePeriod {
|
|
t.Fatalf("recycle = %s", d)
|
|
}
|
|
if d := svc.AuditRetention(ctx); d != cfg.Policy.AuditRetention {
|
|
t.Fatalf("retention = %s", d)
|
|
}
|
|
_ = svc.Set(ctx, "policy.recycle_period", "168h")
|
|
_ = svc.Set(ctx, "policy.audit_retention", "720h")
|
|
if d := svc.RecyclePeriod(ctx); d != 168*time.Hour {
|
|
t.Fatalf("recycle after set = %s", d)
|
|
}
|
|
}
|
|
|
|
func TestUserServiceDefaultTTLFromSettings(t *testing.T) {
|
|
db := testDB(t)
|
|
sys := newFakeSys()
|
|
cfg := testConfig()
|
|
users := NewUserService(db, sys, cfg)
|
|
settings := NewSettingService(db, cfg)
|
|
users.WithSettings(settings)
|
|
ctx := context.Background()
|
|
|
|
_ = settings.Set(ctx, "policy.default_ttl", "168h")
|
|
u, err := users.Create(ctx, "ttluser", "ttl@example.com", "", "", 0, 1)
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
if u.ExpireAt == nil {
|
|
t.Fatal("expire_at nil")
|
|
}
|
|
if d := time.Until(*u.ExpireAt); d < 160*time.Hour || d > 176*time.Hour {
|
|
t.Fatalf("expire in = %s, want ~168h", d)
|
|
}
|
|
}
|
|
|
|
func TestAuditServiceQueryExport(t *testing.T) {
|
|
db := testDB(t)
|
|
svc := NewAuditService(db, testLogger())
|
|
ctx := context.Background()
|
|
|
|
for i := 0; i < 5; i++ {
|
|
if err := svc.Record(ctx, 1, "root", "user.create", "user", "10", map[string]any{"n": i}, "127.0.0.1", model.ResultSuccess); err != nil {
|
|
t.Fatalf("record: %v", err)
|
|
}
|
|
}
|
|
if err := svc.Record(ctx, 2, "ext_x", "key.create", "ssh_key", "3", nil, "10.0.0.1", model.ResultSuccess); err != nil {
|
|
t.Fatalf("record: %v", err)
|
|
}
|
|
|
|
// 分页查询
|
|
rows, total, err := svc.Query(ctx, AuditFilter{Action: "user.create", Page: 1, PageSize: 2})
|
|
if err != nil {
|
|
t.Fatalf("query: %v", err)
|
|
}
|
|
if total != 5 || len(rows) != 2 {
|
|
t.Fatalf("query total=%d len=%d, want 5/2", total, len(rows))
|
|
}
|
|
// 按 actor 筛选
|
|
rows, total, _ = svc.Query(ctx, AuditFilter{ActorName: "ext_", Page: 1, PageSize: 10})
|
|
if total != 1 {
|
|
t.Fatalf("actor filter total = %d, want 1", total)
|
|
}
|
|
|
|
// CSV 导出(含 BOM 与表头)
|
|
var buf strings.Builder
|
|
if err := svc.ExportCSV(ctx, &buf, nil, nil); err != nil {
|
|
t.Fatalf("export: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.HasPrefix(out, "\ufeffid,created_at,") {
|
|
t.Fatalf("csv missing header/BOM: %q", out[:40])
|
|
}
|
|
if !strings.Contains(out, "user.create") || !strings.Contains(out, "key.create") {
|
|
t.Fatalf("csv missing rows: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestAuditServiceArchive(t *testing.T) {
|
|
db := testDB(t)
|
|
svc := NewAuditService(db, testLogger())
|
|
ctx := context.Background()
|
|
dir := t.TempDir()
|
|
|
|
// 3 条旧记录 + 1 条新记录
|
|
old := time.Now().Add(-10 * 24 * time.Hour)
|
|
for i := 0; i < 3; i++ {
|
|
if err := db.Create(&model.AuditLog{CreatedAt: old, Action: "old", ActorName: "root", Result: model.ResultSuccess}).Error; err != nil {
|
|
t.Fatalf("seed old: %v", err)
|
|
}
|
|
}
|
|
if err := svc.Record(ctx, 1, "root", "fresh", "user", "1", nil, "ip", model.ResultSuccess); err != nil {
|
|
t.Fatalf("seed fresh: %v", err)
|
|
}
|
|
|
|
n, err := svc.Archive(ctx, dir, time.Now().Add(-5*24*time.Hour))
|
|
if err != nil {
|
|
t.Fatalf("archive: %v", err)
|
|
}
|
|
if n != 3 {
|
|
t.Fatalf("archived = %d, want 3", n)
|
|
}
|
|
// 归档后旧记录已清理,新记录保留
|
|
var count int64
|
|
if err := db.Model(&model.AuditLog{}).Count(&count).Error; err != nil {
|
|
t.Fatalf("count: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("remaining = %d, want 1", count)
|
|
}
|
|
// 归档文件存在且含旧记录
|
|
files, _ := filepath.Glob(filepath.Join(dir, "audit-*.csv"))
|
|
if len(files) != 1 {
|
|
t.Fatalf("archive files = %v", files)
|
|
}
|
|
b, _ := os.ReadFile(files[0])
|
|
if !strings.Contains(string(b), "old") {
|
|
t.Fatalf("archive file missing rows: %q", string(b))
|
|
}
|
|
|
|
// dir 为空:跳过且不清理
|
|
n, err = svc.Archive(ctx, "", time.Now().Add(-5*24*time.Hour))
|
|
if err != nil || n != 0 {
|
|
t.Fatalf("empty dir archive = %d, %v, want 0/nil", n, err)
|
|
}
|
|
count = 0
|
|
if err := db.Model(&model.AuditLog{}).Count(&count).Error; err != nil {
|
|
t.Fatalf("count: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("remaining after empty-dir = %d, want 1(未配置目录不清理)", count)
|
|
}
|
|
}
|
|
|
|
func TestLifecycleScanExpired(t *testing.T) {
|
|
db := testDB(t)
|
|
sys := newFakeSys()
|
|
cfg := testConfig()
|
|
users := NewUserService(db, sys, cfg)
|
|
settings := NewSettingService(db, cfg)
|
|
mailer := &recordingMail{}
|
|
log := testLogger()
|
|
svc := NewLifecycleService(db, cfg, sys, users, settings, mailer, NewAuditService(db, log), log)
|
|
ctx := context.Background()
|
|
|
|
// 一个将过期、一个正常
|
|
u1, err := users.Create(ctx, "expire1", "e1@example.com", "", "", 90*24*time.Hour, 1)
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
u2, err := users.Create(ctx, "expire2", "e2@example.com", "", "", 90*24*time.Hour, 1)
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
past := time.Now().Add(-time.Hour)
|
|
if err := db.Model(&model.User{}).Where("id = ?", u1.ID).Update("expire_at", &past).Error; err != nil {
|
|
t.Fatalf("force expire u1: %v", err)
|
|
}
|
|
|
|
n, err := svc.ScanExpired(ctx)
|
|
if err != nil {
|
|
t.Fatalf("scan: %v", err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("expired = %d, want 1", n)
|
|
}
|
|
if got, _ := users.GetByID(ctx, u1.ID); got.Status != model.UserStatusExpired {
|
|
t.Fatalf("u1 status = %s, want expired", got.Status)
|
|
}
|
|
if got, _ := users.GetByID(ctx, u2.ID); got.Status != model.UserStatusActive {
|
|
t.Fatalf("u2 status = %s, want active", got.Status)
|
|
}
|
|
if mailer.lastTo != "e1@example.com" || !strings.Contains(mailer.lastBody, "到期") {
|
|
t.Fatalf("expire mail = %s / %s", mailer.lastTo, mailer.lastBody)
|
|
}
|
|
|
|
// 幂等:再次扫描不重复处理(已 expired)
|
|
if n, _ := svc.ScanExpired(ctx); n != 0 {
|
|
t.Fatalf("rescan = %d, want 0", n)
|
|
}
|
|
}
|
|
|
|
func TestLifecycleRecycle(t *testing.T) {
|
|
db := testDB(t)
|
|
sys := newFakeSys()
|
|
cfg := testConfig()
|
|
users := NewUserService(db, sys, cfg)
|
|
settings := NewSettingService(db, cfg)
|
|
mailer := &recordingMail{}
|
|
log := testLogger()
|
|
svc := NewLifecycleService(db, cfg, sys, users, settings, mailer, NewAuditService(db, log), log)
|
|
ctx := context.Background()
|
|
|
|
// 已过期很久(超回收期)与刚过期(回收期内)
|
|
u1, err := users.Create(ctx, "oldone", "o1@example.com", "", "", 90*24*time.Hour, 1)
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
u2, err := users.Create(ctx, "freshone", "f1@example.com", "", "", 90*24*time.Hour, 1)
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
longPast := time.Now().Add(-100 * 24 * time.Hour)
|
|
justPast := time.Now().Add(-time.Hour)
|
|
if err := db.Model(&model.User{}).Where("id = ?", u1.ID).Updates(map[string]any{"status": model.UserStatusExpired, "expire_at": &longPast}).Error; err != nil {
|
|
t.Fatalf("force u1: %v", err)
|
|
}
|
|
if err := db.Model(&model.User{}).Where("id = ?", u2.ID).Updates(map[string]any{"status": model.UserStatusExpired, "expire_at": &justPast}).Error; err != nil {
|
|
t.Fatalf("force u2: %v", err)
|
|
}
|
|
|
|
n, err := svc.Recycle(ctx)
|
|
if err != nil {
|
|
t.Fatalf("recycle: %v", err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("recycled = %d, want 1", n)
|
|
}
|
|
// 超期账号已删除(DB + 系统账号),回收期内账号保留
|
|
if _, err := users.GetByID(ctx, u1.ID); !errors.Is(err, ErrUserNotFound) {
|
|
t.Fatalf("u1 err = %v, want ErrUserNotFound", err)
|
|
}
|
|
if sys.has("ext_oldone") {
|
|
t.Fatal("u1 system account should be removed")
|
|
}
|
|
if _, err := users.GetByID(ctx, u2.ID); err != nil {
|
|
t.Fatalf("u2 should remain: %v", err)
|
|
}
|
|
if mailer.lastTo != "o1@example.com" || !strings.Contains(mailer.lastBody, "回收") {
|
|
t.Fatalf("recycle mail = %s / %s", mailer.lastTo, mailer.lastBody)
|
|
}
|
|
}
|