ops_cleanup_overlay_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "encoding/json"
  6. "testing"
  7. "github.com/Wei-Shaw/sub2api/internal/config"
  8. )
  9. // makeOverlayService 构造一个没有 cron / db 的 cleanup service,仅用来测试 effective overlay。
  10. func makeOverlayService(repo SettingRepository, base config.OpsCleanupConfig) *OpsCleanupService {
  11. cfg := &config.Config{}
  12. cfg.Ops.Cleanup = base
  13. return &OpsCleanupService{
  14. cfg: cfg,
  15. settingRepo: repo,
  16. }
  17. }
  18. func writeAdvancedSettings(t *testing.T, repo *runtimeSettingRepoStub, dr OpsDataRetentionSettings) {
  19. t.Helper()
  20. adv := OpsAdvancedSettings{DataRetention: dr}
  21. raw, err := json.Marshal(adv)
  22. if err != nil {
  23. t.Fatalf("marshal: %v", err)
  24. }
  25. if err := repo.Set(context.Background(), SettingKeyOpsAdvancedSettings, string(raw)); err != nil {
  26. t.Fatalf("set: %v", err)
  27. }
  28. }
  29. func TestComputeEffective_FallbackToCfgWhenSettingsAbsent(t *testing.T) {
  30. repo := newRuntimeSettingRepoStub()
  31. base := config.OpsCleanupConfig{
  32. Enabled: false,
  33. Schedule: "0 2 * * *",
  34. ErrorLogRetentionDays: 30,
  35. MinuteMetricsRetentionDays: 30,
  36. HourlyMetricsRetentionDays: 30,
  37. }
  38. svc := makeOverlayService(repo, base)
  39. svc.computeEffectiveLocked(context.Background())
  40. want := opsCleanupEffectiveConfig{OpsCleanupConfig: base, SystemLogRetentionDays: 30}
  41. if svc.effective != want {
  42. t.Fatalf("expected effective == cfg base, got %#v", svc.effective)
  43. }
  44. }
  45. func TestComputeEffective_SettingsOverridesAll(t *testing.T) {
  46. repo := newRuntimeSettingRepoStub()
  47. writeAdvancedSettings(t, repo, OpsDataRetentionSettings{
  48. CleanupEnabled: true,
  49. CleanupSchedule: "0 * * * *",
  50. ErrorLogRetentionDays: 0,
  51. MinuteMetricsRetentionDays: 7,
  52. HourlyMetricsRetentionDays: 14,
  53. })
  54. base := config.OpsCleanupConfig{
  55. Enabled: false,
  56. Schedule: "0 2 * * *",
  57. ErrorLogRetentionDays: 30,
  58. MinuteMetricsRetentionDays: 30,
  59. HourlyMetricsRetentionDays: 30,
  60. }
  61. svc := makeOverlayService(repo, base)
  62. svc.computeEffectiveLocked(context.Background())
  63. want := opsCleanupEffectiveConfig{
  64. OpsCleanupConfig: config.OpsCleanupConfig{
  65. Enabled: true,
  66. Schedule: "0 * * * *",
  67. ErrorLogRetentionDays: 0,
  68. MinuteMetricsRetentionDays: 7,
  69. HourlyMetricsRetentionDays: 14,
  70. },
  71. SystemLogRetentionDays: 30,
  72. }
  73. if svc.effective != want {
  74. t.Fatalf("effective mismatch:\nwant %#v\n got %#v", want, svc.effective)
  75. }
  76. }
  77. func TestComputeEffective_EmptyScheduleFallbackToCfg(t *testing.T) {
  78. repo := newRuntimeSettingRepoStub()
  79. writeAdvancedSettings(t, repo, OpsDataRetentionSettings{
  80. CleanupEnabled: true,
  81. CleanupSchedule: " ", // 空白被 trim 后视为空
  82. ErrorLogRetentionDays: 5,
  83. MinuteMetricsRetentionDays: 5,
  84. HourlyMetricsRetentionDays: 5,
  85. })
  86. base := config.OpsCleanupConfig{
  87. Enabled: false,
  88. Schedule: "0 2 * * *",
  89. ErrorLogRetentionDays: 30,
  90. MinuteMetricsRetentionDays: 30,
  91. HourlyMetricsRetentionDays: 30,
  92. }
  93. svc := makeOverlayService(repo, base)
  94. svc.computeEffectiveLocked(context.Background())
  95. if svc.effective.Schedule != "0 2 * * *" {
  96. t.Fatalf("expected schedule fallback to cfg, got %q", svc.effective.Schedule)
  97. }
  98. if !svc.effective.Enabled {
  99. t.Fatalf("expected enabled=true from settings")
  100. }
  101. if svc.effective.ErrorLogRetentionDays != 5 {
  102. t.Fatalf("expected retention=5 from settings, got %d", svc.effective.ErrorLogRetentionDays)
  103. }
  104. }
  105. func TestComputeEffective_NegativeRetentionFallsBackToCfg(t *testing.T) {
  106. repo := newRuntimeSettingRepoStub()
  107. writeAdvancedSettings(t, repo, OpsDataRetentionSettings{
  108. CleanupEnabled: true,
  109. CleanupSchedule: "0 * * * *",
  110. ErrorLogRetentionDays: -1,
  111. MinuteMetricsRetentionDays: -1,
  112. HourlyMetricsRetentionDays: -1,
  113. })
  114. base := config.OpsCleanupConfig{
  115. Enabled: false,
  116. Schedule: "0 2 * * *",
  117. ErrorLogRetentionDays: 30,
  118. MinuteMetricsRetentionDays: 60,
  119. HourlyMetricsRetentionDays: 90,
  120. }
  121. svc := makeOverlayService(repo, base)
  122. svc.computeEffectiveLocked(context.Background())
  123. if svc.effective.ErrorLogRetentionDays != 30 ||
  124. svc.effective.MinuteMetricsRetentionDays != 60 ||
  125. svc.effective.HourlyMetricsRetentionDays != 90 {
  126. t.Fatalf("expected retention fallback to cfg, got %#v", svc.effective)
  127. }
  128. }
  129. func TestComputeEffective_BadJSONFallsBackToCfg(t *testing.T) {
  130. repo := newRuntimeSettingRepoStub()
  131. if err := repo.Set(context.Background(), SettingKeyOpsAdvancedSettings, "{not json"); err != nil {
  132. t.Fatalf("set: %v", err)
  133. }
  134. base := config.OpsCleanupConfig{
  135. Enabled: true,
  136. Schedule: "0 3 * * *",
  137. ErrorLogRetentionDays: 30,
  138. MinuteMetricsRetentionDays: 30,
  139. HourlyMetricsRetentionDays: 30,
  140. }
  141. svc := makeOverlayService(repo, base)
  142. svc.computeEffectiveLocked(context.Background())
  143. want := opsCleanupEffectiveConfig{OpsCleanupConfig: base, SystemLogRetentionDays: 30}
  144. if svc.effective != want {
  145. t.Fatalf("expected fallback to cfg on bad JSON, got %#v", svc.effective)
  146. }
  147. }
  148. func TestComputeEffective_RuntimeConfigOwnsSystemLogRetention(t *testing.T) {
  149. repo := newRuntimeSettingRepoStub()
  150. writeAdvancedSettings(t, repo, OpsDataRetentionSettings{
  151. CleanupEnabled: true,
  152. CleanupSchedule: "0 2 * * *",
  153. ErrorLogRetentionDays: 90,
  154. })
  155. repo.values[SettingKeyOpsRuntimeLogConfig] = `{"retention_days":7}`
  156. svc := makeOverlayService(repo, config.OpsCleanupConfig{ErrorLogRetentionDays: 30})
  157. svc.computeEffectiveLocked(context.Background())
  158. if svc.effective.ErrorLogRetentionDays != 90 {
  159. t.Fatalf("error log retention = %d, want 90", svc.effective.ErrorLogRetentionDays)
  160. }
  161. if svc.effective.SystemLogRetentionDays != 7 {
  162. t.Fatalf("system log retention = %d, want 7", svc.effective.SystemLogRetentionDays)
  163. }
  164. }
  165. func TestComputeEffective_InvalidRuntimeConfigDoesNotDiscardAdvancedSettings(t *testing.T) {
  166. repo := newRuntimeSettingRepoStub()
  167. writeAdvancedSettings(t, repo, OpsDataRetentionSettings{
  168. CleanupEnabled: true,
  169. CleanupSchedule: "0 * * * *",
  170. ErrorLogRetentionDays: 14,
  171. })
  172. repo.values[SettingKeyOpsRuntimeLogConfig] = `{invalid-json}`
  173. svc := makeOverlayService(repo, config.OpsCleanupConfig{ErrorLogRetentionDays: 30})
  174. svc.computeEffectiveLocked(context.Background())
  175. if !svc.effective.Enabled || svc.effective.ErrorLogRetentionDays != 14 {
  176. t.Fatalf("advanced settings were discarded: %#v", svc.effective)
  177. }
  178. if svc.effective.SystemLogRetentionDays != 30 {
  179. t.Fatalf("system log retention = %d, want default 30", svc.effective.SystemLogRetentionDays)
  180. }
  181. }
  182. func TestComputeEffective_PartialAdvancedSettingsUseSafeDefaults(t *testing.T) {
  183. repo := newRuntimeSettingRepoStub()
  184. repo.values[SettingKeyOpsAdvancedSettings] = `{"data_retention":{"cleanup_enabled":true}}`
  185. svc := makeOverlayService(repo, config.OpsCleanupConfig{
  186. Schedule: "0 3 * * *",
  187. ErrorLogRetentionDays: 90,
  188. MinuteMetricsRetentionDays: 90,
  189. HourlyMetricsRetentionDays: 90,
  190. })
  191. svc.computeEffectiveLocked(context.Background())
  192. if svc.effective.Schedule != opsCleanupDefaultSchedule ||
  193. svc.effective.ErrorLogRetentionDays != 30 ||
  194. svc.effective.MinuteMetricsRetentionDays != 30 ||
  195. svc.effective.HourlyMetricsRetentionDays != 30 {
  196. t.Fatalf("partial settings did not use safe defaults: %#v", svc.effective)
  197. }
  198. }
  199. // 验证 OpsService.UpdateOpsAdvancedSettings 写入后会调用 cleanupReloader.Reload。
  200. type fakeCleanupReloader struct {
  201. calls int
  202. last context.Context
  203. err error
  204. }
  205. func (f *fakeCleanupReloader) Reload(ctx context.Context) error {
  206. f.calls++
  207. f.last = ctx
  208. return f.err
  209. }
  210. func TestUpdateOpsAdvancedSettings_TriggersReload(t *testing.T) {
  211. repo := newRuntimeSettingRepoStub()
  212. reloader := &fakeCleanupReloader{}
  213. svc := &OpsService{settingRepo: repo}
  214. svc.SetCleanupReloader(reloader)
  215. cfg := defaultOpsAdvancedSettings()
  216. cfg.DataRetention.CleanupEnabled = true
  217. cfg.DataRetention.CleanupSchedule = "0 * * * *"
  218. cfg.DataRetention.ErrorLogRetentionDays = 3
  219. cfg.DataRetention.MinuteMetricsRetentionDays = 3
  220. cfg.DataRetention.HourlyMetricsRetentionDays = 3
  221. if _, err := svc.UpdateOpsAdvancedSettings(context.Background(), cfg); err != nil {
  222. t.Fatalf("update: %v", err)
  223. }
  224. if reloader.calls != 1 {
  225. t.Fatalf("expected reloader.Reload called once, got %d", reloader.calls)
  226. }
  227. }
  228. func TestReload_BeforeStart_IsNoop(t *testing.T) {
  229. svc := &OpsCleanupService{}
  230. if err := svc.Reload(context.Background()); err != nil {
  231. t.Fatalf("Reload before Start should return nil, got %v", err)
  232. }
  233. }
  234. func TestReload_AfterStop_IsNoop(t *testing.T) {
  235. svc := &OpsCleanupService{started: true, stopped: true}
  236. if err := svc.Reload(context.Background()); err != nil {
  237. t.Fatalf("Reload after Stop should return nil, got %v", err)
  238. }
  239. }
  240. func TestUpdateOpsAdvancedSettings_NilReloader_NoPanic(t *testing.T) {
  241. repo := newRuntimeSettingRepoStub()
  242. svc := &OpsService{settingRepo: repo}
  243. // cleanupReloader intentionally nil
  244. cfg := defaultOpsAdvancedSettings()
  245. cfg.DataRetention.ErrorLogRetentionDays = 7
  246. // should not panic
  247. if _, err := svc.UpdateOpsAdvancedSettings(context.Background(), cfg); err != nil {
  248. t.Fatalf("update with nil reloader: %v", err)
  249. }
  250. }
  251. func TestStart_IdempotentSecondCall(t *testing.T) {
  252. svc := &OpsCleanupService{started: true}
  253. svc.Start() // second call should be noop, not panic
  254. }
  255. func TestRefreshEffectiveBeforeRun_UpdatesSnapshot(t *testing.T) {
  256. repo := newRuntimeSettingRepoStub()
  257. base := config.OpsCleanupConfig{
  258. Enabled: true,
  259. Schedule: "0 2 * * *",
  260. ErrorLogRetentionDays: 30,
  261. }
  262. svc := makeOverlayService(repo, base)
  263. svc.computeEffectiveLocked(context.Background())
  264. if svc.effective.ErrorLogRetentionDays != 30 {
  265. t.Fatalf("initial retention should be 30, got %d", svc.effective.ErrorLogRetentionDays)
  266. }
  267. // simulate UI change
  268. writeAdvancedSettings(t, repo, OpsDataRetentionSettings{
  269. CleanupEnabled: true,
  270. CleanupSchedule: "0 * * * *",
  271. ErrorLogRetentionDays: 7,
  272. })
  273. svc.refreshEffectiveBeforeRun(context.Background())
  274. snap := svc.snapshotEffective()
  275. if snap.ErrorLogRetentionDays != 7 {
  276. t.Fatalf("after refresh, retention should be 7, got %d", snap.ErrorLogRetentionDays)
  277. }
  278. }