| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- //go:build unit
- package service
- import (
- "context"
- "encoding/json"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/config"
- )
- // makeOverlayService 构造一个没有 cron / db 的 cleanup service,仅用来测试 effective overlay。
- func makeOverlayService(repo SettingRepository, base config.OpsCleanupConfig) *OpsCleanupService {
- cfg := &config.Config{}
- cfg.Ops.Cleanup = base
- return &OpsCleanupService{
- cfg: cfg,
- settingRepo: repo,
- }
- }
- func writeAdvancedSettings(t *testing.T, repo *runtimeSettingRepoStub, dr OpsDataRetentionSettings) {
- t.Helper()
- adv := OpsAdvancedSettings{DataRetention: dr}
- raw, err := json.Marshal(adv)
- if err != nil {
- t.Fatalf("marshal: %v", err)
- }
- if err := repo.Set(context.Background(), SettingKeyOpsAdvancedSettings, string(raw)); err != nil {
- t.Fatalf("set: %v", err)
- }
- }
- func TestComputeEffective_FallbackToCfgWhenSettingsAbsent(t *testing.T) {
- repo := newRuntimeSettingRepoStub()
- base := config.OpsCleanupConfig{
- Enabled: false,
- Schedule: "0 2 * * *",
- ErrorLogRetentionDays: 30,
- MinuteMetricsRetentionDays: 30,
- HourlyMetricsRetentionDays: 30,
- }
- svc := makeOverlayService(repo, base)
- svc.computeEffectiveLocked(context.Background())
- want := opsCleanupEffectiveConfig{OpsCleanupConfig: base, SystemLogRetentionDays: 30}
- if svc.effective != want {
- t.Fatalf("expected effective == cfg base, got %#v", svc.effective)
- }
- }
- func TestComputeEffective_SettingsOverridesAll(t *testing.T) {
- repo := newRuntimeSettingRepoStub()
- writeAdvancedSettings(t, repo, OpsDataRetentionSettings{
- CleanupEnabled: true,
- CleanupSchedule: "0 * * * *",
- ErrorLogRetentionDays: 0,
- MinuteMetricsRetentionDays: 7,
- HourlyMetricsRetentionDays: 14,
- })
- base := config.OpsCleanupConfig{
- Enabled: false,
- Schedule: "0 2 * * *",
- ErrorLogRetentionDays: 30,
- MinuteMetricsRetentionDays: 30,
- HourlyMetricsRetentionDays: 30,
- }
- svc := makeOverlayService(repo, base)
- svc.computeEffectiveLocked(context.Background())
- want := opsCleanupEffectiveConfig{
- OpsCleanupConfig: config.OpsCleanupConfig{
- Enabled: true,
- Schedule: "0 * * * *",
- ErrorLogRetentionDays: 0,
- MinuteMetricsRetentionDays: 7,
- HourlyMetricsRetentionDays: 14,
- },
- SystemLogRetentionDays: 30,
- }
- if svc.effective != want {
- t.Fatalf("effective mismatch:\nwant %#v\n got %#v", want, svc.effective)
- }
- }
- func TestComputeEffective_EmptyScheduleFallbackToCfg(t *testing.T) {
- repo := newRuntimeSettingRepoStub()
- writeAdvancedSettings(t, repo, OpsDataRetentionSettings{
- CleanupEnabled: true,
- CleanupSchedule: " ", // 空白被 trim 后视为空
- ErrorLogRetentionDays: 5,
- MinuteMetricsRetentionDays: 5,
- HourlyMetricsRetentionDays: 5,
- })
- base := config.OpsCleanupConfig{
- Enabled: false,
- Schedule: "0 2 * * *",
- ErrorLogRetentionDays: 30,
- MinuteMetricsRetentionDays: 30,
- HourlyMetricsRetentionDays: 30,
- }
- svc := makeOverlayService(repo, base)
- svc.computeEffectiveLocked(context.Background())
- if svc.effective.Schedule != "0 2 * * *" {
- t.Fatalf("expected schedule fallback to cfg, got %q", svc.effective.Schedule)
- }
- if !svc.effective.Enabled {
- t.Fatalf("expected enabled=true from settings")
- }
- if svc.effective.ErrorLogRetentionDays != 5 {
- t.Fatalf("expected retention=5 from settings, got %d", svc.effective.ErrorLogRetentionDays)
- }
- }
- func TestComputeEffective_NegativeRetentionFallsBackToCfg(t *testing.T) {
- repo := newRuntimeSettingRepoStub()
- writeAdvancedSettings(t, repo, OpsDataRetentionSettings{
- CleanupEnabled: true,
- CleanupSchedule: "0 * * * *",
- ErrorLogRetentionDays: -1,
- MinuteMetricsRetentionDays: -1,
- HourlyMetricsRetentionDays: -1,
- })
- base := config.OpsCleanupConfig{
- Enabled: false,
- Schedule: "0 2 * * *",
- ErrorLogRetentionDays: 30,
- MinuteMetricsRetentionDays: 60,
- HourlyMetricsRetentionDays: 90,
- }
- svc := makeOverlayService(repo, base)
- svc.computeEffectiveLocked(context.Background())
- if svc.effective.ErrorLogRetentionDays != 30 ||
- svc.effective.MinuteMetricsRetentionDays != 60 ||
- svc.effective.HourlyMetricsRetentionDays != 90 {
- t.Fatalf("expected retention fallback to cfg, got %#v", svc.effective)
- }
- }
- func TestComputeEffective_BadJSONFallsBackToCfg(t *testing.T) {
- repo := newRuntimeSettingRepoStub()
- if err := repo.Set(context.Background(), SettingKeyOpsAdvancedSettings, "{not json"); err != nil {
- t.Fatalf("set: %v", err)
- }
- base := config.OpsCleanupConfig{
- Enabled: true,
- Schedule: "0 3 * * *",
- ErrorLogRetentionDays: 30,
- MinuteMetricsRetentionDays: 30,
- HourlyMetricsRetentionDays: 30,
- }
- svc := makeOverlayService(repo, base)
- svc.computeEffectiveLocked(context.Background())
- want := opsCleanupEffectiveConfig{OpsCleanupConfig: base, SystemLogRetentionDays: 30}
- if svc.effective != want {
- t.Fatalf("expected fallback to cfg on bad JSON, got %#v", svc.effective)
- }
- }
- func TestComputeEffective_RuntimeConfigOwnsSystemLogRetention(t *testing.T) {
- repo := newRuntimeSettingRepoStub()
- writeAdvancedSettings(t, repo, OpsDataRetentionSettings{
- CleanupEnabled: true,
- CleanupSchedule: "0 2 * * *",
- ErrorLogRetentionDays: 90,
- })
- repo.values[SettingKeyOpsRuntimeLogConfig] = `{"retention_days":7}`
- svc := makeOverlayService(repo, config.OpsCleanupConfig{ErrorLogRetentionDays: 30})
- svc.computeEffectiveLocked(context.Background())
- if svc.effective.ErrorLogRetentionDays != 90 {
- t.Fatalf("error log retention = %d, want 90", svc.effective.ErrorLogRetentionDays)
- }
- if svc.effective.SystemLogRetentionDays != 7 {
- t.Fatalf("system log retention = %d, want 7", svc.effective.SystemLogRetentionDays)
- }
- }
- func TestComputeEffective_InvalidRuntimeConfigDoesNotDiscardAdvancedSettings(t *testing.T) {
- repo := newRuntimeSettingRepoStub()
- writeAdvancedSettings(t, repo, OpsDataRetentionSettings{
- CleanupEnabled: true,
- CleanupSchedule: "0 * * * *",
- ErrorLogRetentionDays: 14,
- })
- repo.values[SettingKeyOpsRuntimeLogConfig] = `{invalid-json}`
- svc := makeOverlayService(repo, config.OpsCleanupConfig{ErrorLogRetentionDays: 30})
- svc.computeEffectiveLocked(context.Background())
- if !svc.effective.Enabled || svc.effective.ErrorLogRetentionDays != 14 {
- t.Fatalf("advanced settings were discarded: %#v", svc.effective)
- }
- if svc.effective.SystemLogRetentionDays != 30 {
- t.Fatalf("system log retention = %d, want default 30", svc.effective.SystemLogRetentionDays)
- }
- }
- func TestComputeEffective_PartialAdvancedSettingsUseSafeDefaults(t *testing.T) {
- repo := newRuntimeSettingRepoStub()
- repo.values[SettingKeyOpsAdvancedSettings] = `{"data_retention":{"cleanup_enabled":true}}`
- svc := makeOverlayService(repo, config.OpsCleanupConfig{
- Schedule: "0 3 * * *",
- ErrorLogRetentionDays: 90,
- MinuteMetricsRetentionDays: 90,
- HourlyMetricsRetentionDays: 90,
- })
- svc.computeEffectiveLocked(context.Background())
- if svc.effective.Schedule != opsCleanupDefaultSchedule ||
- svc.effective.ErrorLogRetentionDays != 30 ||
- svc.effective.MinuteMetricsRetentionDays != 30 ||
- svc.effective.HourlyMetricsRetentionDays != 30 {
- t.Fatalf("partial settings did not use safe defaults: %#v", svc.effective)
- }
- }
- // 验证 OpsService.UpdateOpsAdvancedSettings 写入后会调用 cleanupReloader.Reload。
- type fakeCleanupReloader struct {
- calls int
- last context.Context
- err error
- }
- func (f *fakeCleanupReloader) Reload(ctx context.Context) error {
- f.calls++
- f.last = ctx
- return f.err
- }
- func TestUpdateOpsAdvancedSettings_TriggersReload(t *testing.T) {
- repo := newRuntimeSettingRepoStub()
- reloader := &fakeCleanupReloader{}
- svc := &OpsService{settingRepo: repo}
- svc.SetCleanupReloader(reloader)
- cfg := defaultOpsAdvancedSettings()
- cfg.DataRetention.CleanupEnabled = true
- cfg.DataRetention.CleanupSchedule = "0 * * * *"
- cfg.DataRetention.ErrorLogRetentionDays = 3
- cfg.DataRetention.MinuteMetricsRetentionDays = 3
- cfg.DataRetention.HourlyMetricsRetentionDays = 3
- if _, err := svc.UpdateOpsAdvancedSettings(context.Background(), cfg); err != nil {
- t.Fatalf("update: %v", err)
- }
- if reloader.calls != 1 {
- t.Fatalf("expected reloader.Reload called once, got %d", reloader.calls)
- }
- }
- func TestReload_BeforeStart_IsNoop(t *testing.T) {
- svc := &OpsCleanupService{}
- if err := svc.Reload(context.Background()); err != nil {
- t.Fatalf("Reload before Start should return nil, got %v", err)
- }
- }
- func TestReload_AfterStop_IsNoop(t *testing.T) {
- svc := &OpsCleanupService{started: true, stopped: true}
- if err := svc.Reload(context.Background()); err != nil {
- t.Fatalf("Reload after Stop should return nil, got %v", err)
- }
- }
- func TestUpdateOpsAdvancedSettings_NilReloader_NoPanic(t *testing.T) {
- repo := newRuntimeSettingRepoStub()
- svc := &OpsService{settingRepo: repo}
- // cleanupReloader intentionally nil
- cfg := defaultOpsAdvancedSettings()
- cfg.DataRetention.ErrorLogRetentionDays = 7
- // should not panic
- if _, err := svc.UpdateOpsAdvancedSettings(context.Background(), cfg); err != nil {
- t.Fatalf("update with nil reloader: %v", err)
- }
- }
- func TestStart_IdempotentSecondCall(t *testing.T) {
- svc := &OpsCleanupService{started: true}
- svc.Start() // second call should be noop, not panic
- }
- func TestRefreshEffectiveBeforeRun_UpdatesSnapshot(t *testing.T) {
- repo := newRuntimeSettingRepoStub()
- base := config.OpsCleanupConfig{
- Enabled: true,
- Schedule: "0 2 * * *",
- ErrorLogRetentionDays: 30,
- }
- svc := makeOverlayService(repo, base)
- svc.computeEffectiveLocked(context.Background())
- if svc.effective.ErrorLogRetentionDays != 30 {
- t.Fatalf("initial retention should be 30, got %d", svc.effective.ErrorLogRetentionDays)
- }
- // simulate UI change
- writeAdvancedSettings(t, repo, OpsDataRetentionSettings{
- CleanupEnabled: true,
- CleanupSchedule: "0 * * * *",
- ErrorLogRetentionDays: 7,
- })
- svc.refreshEffectiveBeforeRun(context.Background())
- snap := svc.snapshotEffective()
- if snap.ErrorLogRetentionDays != 7 {
- t.Fatalf("after refresh, retention should be 7, got %d", snap.ErrorLogRetentionDays)
- }
- }
|