ops_system_log_service_test.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. package service
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "strings"
  7. "testing"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/config"
  10. )
  11. func TestOpsServiceListSystemLogs_DefaultClampAndSuccess(t *testing.T) {
  12. var gotFilter *OpsSystemLogFilter
  13. repo := &opsRepoMock{
  14. ListSystemLogsFn: func(ctx context.Context, filter *OpsSystemLogFilter) (*OpsSystemLogList, error) {
  15. gotFilter = filter
  16. return &OpsSystemLogList{
  17. Logs: []*OpsSystemLog{{ID: 1, Level: "warn", Message: "x"}},
  18. Total: 1,
  19. Page: filter.Page,
  20. PageSize: filter.PageSize,
  21. }, nil
  22. },
  23. }
  24. svc := NewOpsService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  25. out, err := svc.ListSystemLogs(context.Background(), &OpsSystemLogFilter{
  26. Page: 0,
  27. PageSize: 999,
  28. })
  29. if err != nil {
  30. t.Fatalf("ListSystemLogs() error: %v", err)
  31. }
  32. if gotFilter == nil {
  33. t.Fatalf("expected repository to receive filter")
  34. }
  35. if gotFilter.Page != 1 || gotFilter.PageSize != 200 {
  36. t.Fatalf("filter normalized unexpectedly: page=%d pageSize=%d", gotFilter.Page, gotFilter.PageSize)
  37. }
  38. if out.Total != 1 || len(out.Logs) != 1 {
  39. t.Fatalf("unexpected result: %+v", out)
  40. }
  41. }
  42. func TestOpsServiceListSystemLogs_MonitoringDisabled(t *testing.T) {
  43. svc := NewOpsService(
  44. &opsRepoMock{},
  45. nil,
  46. &config.Config{Ops: config.OpsConfig{Enabled: false}},
  47. nil, nil, nil, nil, nil, nil, nil, nil,
  48. )
  49. _, err := svc.ListSystemLogs(context.Background(), &OpsSystemLogFilter{})
  50. if err == nil {
  51. t.Fatalf("expected disabled error")
  52. }
  53. }
  54. func TestOpsServiceListSystemLogs_NilRepoReturnsEmpty(t *testing.T) {
  55. svc := NewOpsService(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  56. out, err := svc.ListSystemLogs(context.Background(), nil)
  57. if err != nil {
  58. t.Fatalf("ListSystemLogs() error: %v", err)
  59. }
  60. if out == nil || out.Page != 1 || out.PageSize != 50 || out.Total != 0 || len(out.Logs) != 0 {
  61. t.Fatalf("unexpected nil-repo result: %+v", out)
  62. }
  63. }
  64. func TestOpsServiceListSystemLogs_RepoErrorMapped(t *testing.T) {
  65. repo := &opsRepoMock{
  66. ListSystemLogsFn: func(ctx context.Context, filter *OpsSystemLogFilter) (*OpsSystemLogList, error) {
  67. return nil, errors.New("db down")
  68. },
  69. }
  70. svc := NewOpsService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  71. _, err := svc.ListSystemLogs(context.Background(), &OpsSystemLogFilter{})
  72. if err == nil {
  73. t.Fatalf("expected mapped internal error")
  74. }
  75. if !strings.Contains(err.Error(), "OPS_SYSTEM_LOG_LIST_FAILED") {
  76. t.Fatalf("unexpected error: %v", err)
  77. }
  78. }
  79. func TestOpsServiceCleanupSystemLogs_SuccessAndAudit(t *testing.T) {
  80. var audit *OpsSystemLogCleanupAudit
  81. repo := &opsRepoMock{
  82. DeleteSystemLogsFn: func(ctx context.Context, filter *OpsSystemLogCleanupFilter) (int64, error) {
  83. return 3, nil
  84. },
  85. InsertSystemLogCleanupAuditFn: func(ctx context.Context, input *OpsSystemLogCleanupAudit) error {
  86. audit = input
  87. return nil
  88. },
  89. }
  90. svc := NewOpsService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  91. userID := int64(7)
  92. apiKeyID := int64(8)
  93. now := time.Now().UTC()
  94. filter := &OpsSystemLogCleanupFilter{
  95. StartTime: &now,
  96. Host: "api-node-1",
  97. Level: "warn",
  98. RequestID: "req-1",
  99. ClientRequestID: "creq-1",
  100. UserID: &userID,
  101. APIKeyID: &apiKeyID,
  102. Query: "timeout",
  103. }
  104. deleted, err := svc.CleanupSystemLogs(context.Background(), filter, 99)
  105. if err != nil {
  106. t.Fatalf("CleanupSystemLogs() error: %v", err)
  107. }
  108. if deleted != 3 {
  109. t.Fatalf("deleted=%d, want 3", deleted)
  110. }
  111. if audit == nil {
  112. t.Fatalf("expected cleanup audit")
  113. }
  114. if !strings.Contains(audit.Conditions, `"host":"api-node-1"`) {
  115. t.Fatalf("audit conditions should include host: %s", audit.Conditions)
  116. }
  117. if !strings.Contains(audit.Conditions, `"client_request_id":"creq-1"`) {
  118. t.Fatalf("audit conditions should include client_request_id: %s", audit.Conditions)
  119. }
  120. if !strings.Contains(audit.Conditions, `"user_id":7`) {
  121. t.Fatalf("audit conditions should include user_id: %s", audit.Conditions)
  122. }
  123. if !strings.Contains(audit.Conditions, `"api_key_id":8`) {
  124. t.Fatalf("audit conditions should include api_key_id: %s", audit.Conditions)
  125. }
  126. }
  127. func TestOpsServiceCleanupSystemLogs_RepoUnavailableAndInvalidOperator(t *testing.T) {
  128. svc := NewOpsService(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  129. if _, err := svc.CleanupSystemLogs(context.Background(), &OpsSystemLogCleanupFilter{RequestID: "r"}, 1); err == nil {
  130. t.Fatalf("expected repo unavailable error")
  131. }
  132. svc = NewOpsService(&opsRepoMock{}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  133. if _, err := svc.CleanupSystemLogs(context.Background(), &OpsSystemLogCleanupFilter{RequestID: "r"}, 0); err == nil {
  134. t.Fatalf("expected invalid operator error")
  135. }
  136. }
  137. func TestOpsServiceCleanupSystemLogs_FilterRequired(t *testing.T) {
  138. repo := &opsRepoMock{
  139. DeleteSystemLogsFn: func(ctx context.Context, filter *OpsSystemLogCleanupFilter) (int64, error) {
  140. return 0, errors.New("cleanup requires at least one filter condition")
  141. },
  142. }
  143. svc := NewOpsService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  144. _, err := svc.CleanupSystemLogs(context.Background(), &OpsSystemLogCleanupFilter{}, 1)
  145. if err == nil {
  146. t.Fatalf("expected filter required error")
  147. }
  148. if !strings.Contains(strings.ToLower(err.Error()), "filter") {
  149. t.Fatalf("unexpected error: %v", err)
  150. }
  151. }
  152. func TestOpsServiceCleanupSystemLogs_InvalidRange(t *testing.T) {
  153. repo := &opsRepoMock{}
  154. svc := NewOpsService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  155. start := time.Now().UTC()
  156. end := start.Add(-time.Hour)
  157. _, err := svc.CleanupSystemLogs(context.Background(), &OpsSystemLogCleanupFilter{
  158. StartTime: &start,
  159. EndTime: &end,
  160. }, 1)
  161. if err == nil {
  162. t.Fatalf("expected invalid range error")
  163. }
  164. }
  165. func TestOpsServiceCleanupSystemLogs_NoRowsAndInternalError(t *testing.T) {
  166. repo := &opsRepoMock{
  167. DeleteSystemLogsFn: func(ctx context.Context, filter *OpsSystemLogCleanupFilter) (int64, error) {
  168. return 0, sql.ErrNoRows
  169. },
  170. }
  171. svc := NewOpsService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  172. deleted, err := svc.CleanupSystemLogs(context.Background(), &OpsSystemLogCleanupFilter{
  173. RequestID: "req-1",
  174. }, 1)
  175. if err != nil || deleted != 0 {
  176. t.Fatalf("expected no rows shortcut, deleted=%d err=%v", deleted, err)
  177. }
  178. repo.DeleteSystemLogsFn = func(ctx context.Context, filter *OpsSystemLogCleanupFilter) (int64, error) {
  179. return 0, errors.New("boom")
  180. }
  181. if _, err := svc.CleanupSystemLogs(context.Background(), &OpsSystemLogCleanupFilter{
  182. RequestID: "req-1",
  183. }, 1); err == nil {
  184. t.Fatalf("expected internal cleanup error")
  185. }
  186. }
  187. func TestOpsServiceCleanupSystemLogs_AuditFailureIgnored(t *testing.T) {
  188. repo := &opsRepoMock{
  189. DeleteSystemLogsFn: func(ctx context.Context, filter *OpsSystemLogCleanupFilter) (int64, error) {
  190. return 5, nil
  191. },
  192. InsertSystemLogCleanupAuditFn: func(ctx context.Context, input *OpsSystemLogCleanupAudit) error {
  193. return errors.New("audit down")
  194. },
  195. }
  196. svc := NewOpsService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  197. deleted, err := svc.CleanupSystemLogs(context.Background(), &OpsSystemLogCleanupFilter{
  198. RequestID: "r1",
  199. }, 1)
  200. if err != nil || deleted != 5 {
  201. t.Fatalf("audit failure should not break cleanup, deleted=%d err=%v", deleted, err)
  202. }
  203. }
  204. func TestMarshalSystemLogCleanupConditions_NilAndMarshalError(t *testing.T) {
  205. if got := marshalSystemLogCleanupConditions(nil); got != "{}" {
  206. t.Fatalf("nil filter should return {}, got %s", got)
  207. }
  208. now := time.Now().UTC()
  209. userID := int64(1)
  210. apiKeyID := int64(2)
  211. filter := &OpsSystemLogCleanupFilter{
  212. StartTime: &now,
  213. EndTime: &now,
  214. UserID: &userID,
  215. APIKeyID: &apiKeyID,
  216. }
  217. got := marshalSystemLogCleanupConditions(filter)
  218. if !strings.Contains(got, `"start_time"`) || !strings.Contains(got, `"user_id":1`) || !strings.Contains(got, `"api_key_id":2`) {
  219. t.Fatalf("unexpected marshal payload: %s", got)
  220. }
  221. }
  222. func TestOpsServiceGetSystemLogSinkHealth(t *testing.T) {
  223. svc := NewOpsService(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  224. health := svc.GetSystemLogSinkHealth()
  225. if health.QueueCapacity != 0 || health.QueueDepth != 0 {
  226. t.Fatalf("unexpected health for nil sink: %+v", health)
  227. }
  228. sink := NewOpsSystemLogSink(&opsRepoMock{})
  229. svc = NewOpsService(&opsRepoMock{}, nil, nil, nil, nil, nil, nil, nil, nil, nil, sink)
  230. health = svc.GetSystemLogSinkHealth()
  231. if health.QueueCapacity <= 0 {
  232. t.Fatalf("expected non-zero queue capacity: %+v", health)
  233. }
  234. }