calculations_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. package analytics
  2. import (
  3. "context"
  4. "testing"
  5. "github.com/0xJacky/Nginx-UI/internal/nginx_log/searcher"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/mock"
  8. )
  9. func TestService_GetDashboardAnalytics_HourlyStats(t *testing.T) {
  10. mockSearcher := &MockSearcher{}
  11. s := NewService(mockSearcher)
  12. ctx := context.Background()
  13. req := &DashboardQueryRequest{
  14. StartTime: 1640995200, // 2022-01-01 00:00:00 UTC
  15. EndTime: 1641006000, // 2022-01-01 03:00:00 UTC (same day as test data)
  16. LogPaths: []string{"/var/log/nginx/access.log"},
  17. }
  18. expectedResult := &searcher.SearchResult{
  19. TotalHits: 3,
  20. Hits: []*searcher.SearchHit{
  21. {
  22. Fields: map[string]interface{}{
  23. "timestamp": float64(1640995800), // 2022-01-01 00:10:00
  24. "ip": "192.168.1.1",
  25. "bytes": int64(1024),
  26. },
  27. },
  28. {
  29. Fields: map[string]interface{}{
  30. "timestamp": float64(1640999400), // 2022-01-01 01:10:00
  31. "ip": "192.168.1.2",
  32. "bytes": int64(2048),
  33. },
  34. },
  35. {
  36. Fields: map[string]interface{}{
  37. "timestamp": float64(1640999500), // 2022-01-01 01:11:40
  38. "ip": "192.168.1.1", // Same IP as first hit
  39. "bytes": int64(512),
  40. },
  41. },
  42. },
  43. Facets: map[string]*searcher.Facet{
  44. "ip": {
  45. Terms: []*searcher.FacetTerm{
  46. {Term: "192.168.1.1", Count: 2},
  47. {Term: "192.168.1.2", Count: 1},
  48. },
  49. },
  50. },
  51. }
  52. mockSearcher.On("Search", ctx, mock.AnythingOfType("*searcher.SearchRequest")).Return(expectedResult, nil)
  53. result, err := s.GetDashboardAnalytics(ctx, req)
  54. assert.NoError(t, err)
  55. assert.NotNil(t, result)
  56. assert.NotEmpty(t, result.HourlyStats)
  57. // Check that we have some hourly data - the specific hours depend on the test data timestamps
  58. var totalPV, totalUV int
  59. for _, stat := range result.HourlyStats {
  60. totalPV += stat.PV
  61. totalUV += stat.UV
  62. }
  63. // We should have some aggregated data
  64. assert.Greater(t, totalPV, 0)
  65. assert.Greater(t, totalUV, 0)
  66. mockSearcher.AssertExpectations(t)
  67. }
  68. // Duplicate test functions removed - they exist in dashboard_test.go
  69. func TestService_calculateHourlyStats_HourlyInterval(t *testing.T) {
  70. mockSearcher := &MockSearcher{}
  71. s := NewService(mockSearcher).(*service)
  72. result := &searcher.SearchResult{
  73. TotalHits: 2,
  74. Hits: []*searcher.SearchHit{
  75. {
  76. Fields: map[string]interface{}{
  77. "timestamp": float64(1640995800), // 2022-01-01 00:10:00 UTC
  78. "ip": "192.168.1.1",
  79. },
  80. },
  81. {
  82. Fields: map[string]interface{}{
  83. "timestamp": float64(1641002400), // 2022-01-01 02:00:00 UTC
  84. "ip": "192.168.1.2",
  85. },
  86. },
  87. },
  88. }
  89. startTime := int64(1640995200) // 2022-01-01 00:00:00 UTC
  90. endTime := int64(1641002400) // 2022-01-01 02:00:00 UTC
  91. stats := s.calculateHourlyStats(result, startTime, endTime)
  92. assert.NotNil(t, stats)
  93. assert.GreaterOrEqual(t, len(stats), 2) // Should have at least 2 hours
  94. // Check that stats are sorted by timestamp (not just hour, since we have 48 hours of data)
  95. for i := 1; i < len(stats); i++ {
  96. assert.LessOrEqual(t, stats[i-1].Timestamp, stats[i].Timestamp)
  97. }
  98. }
  99. func TestService_calculateDailyStats_DailyInterval(t *testing.T) {
  100. mockSearcher := &MockSearcher{}
  101. s := NewService(mockSearcher).(*service)
  102. result := &searcher.SearchResult{
  103. TotalHits: 2,
  104. Hits: []*searcher.SearchHit{
  105. {
  106. Fields: map[string]interface{}{
  107. "timestamp": float64(1640995800), // 2022-01-01 00:10:00 UTC
  108. "ip": "192.168.1.1",
  109. },
  110. },
  111. {
  112. Fields: map[string]interface{}{
  113. "timestamp": float64(1641168000), // 2022-01-03 00:00:00 UTC
  114. "ip": "192.168.1.2",
  115. },
  116. },
  117. },
  118. }
  119. startTime := int64(1640995200) // 2022-01-01 00:00:00 UTC
  120. endTime := int64(1641168000) // 2022-01-03 00:00:00 UTC
  121. stats := s.calculateDailyStats(result, startTime, endTime)
  122. assert.NotNil(t, stats)
  123. assert.GreaterOrEqual(t, len(stats), 2) // Should have at least 2 days
  124. // Check that stats are sorted by timestamp
  125. for i := 1; i < len(stats); i++ {
  126. assert.LessOrEqual(t, stats[i-1].Timestamp, stats[i].Timestamp)
  127. }
  128. }
  129. func TestService_calculateDashboardSummary_MonthlyData(t *testing.T) {
  130. mockSearcher := &MockSearcher{}
  131. s := NewService(mockSearcher).(*service)
  132. analytics := &DashboardAnalytics{
  133. HourlyStats: []HourlyAccessStats{
  134. {Hour: 0, UV: 10, PV: 100},
  135. {Hour: 1, UV: 20, PV: 200},
  136. {Hour: 2, UV: 15, PV: 150},
  137. },
  138. DailyStats: []DailyAccessStats{
  139. {Date: "2022-01-01", UV: 30, PV: 300, Timestamp: 1640995200},
  140. {Date: "2022-01-02", UV: 25, PV: 250, Timestamp: 1641081600},
  141. {Date: "2022-01-03", UV: 28, PV: 280, Timestamp: 1641168000},
  142. },
  143. }
  144. result := &searcher.SearchResult{
  145. TotalHits: 830,
  146. Facets: map[string]*searcher.Facet{
  147. "ip": {
  148. Total: 50, // 50 unique IPs
  149. },
  150. },
  151. }
  152. summary := s.calculateDashboardSummary(analytics, result)
  153. assert.Equal(t, 50, summary.TotalUV)
  154. assert.Equal(t, 830, summary.TotalPV)
  155. assert.InDelta(t, 16.67, summary.AvgDailyUV, 0.01) // 50 total UV / 3 days
  156. assert.InDelta(t, 276.67, summary.AvgDailyPV, 0.01) // (300+250+280)/3
  157. // Peak hour should be hour 1 with 200 PV
  158. assert.Equal(t, 1, summary.PeakHour)
  159. assert.Equal(t, 200, summary.PeakHourTraffic)
  160. }
  161. func TestService_calculateTopFieldStats_Generic(t *testing.T) {
  162. facet := &searcher.Facet{
  163. Terms: []*searcher.FacetTerm{
  164. {Term: "/api/users", Count: 100},
  165. {Term: "/api/posts", Count: 50},
  166. {Term: "/", Count: 25},
  167. },
  168. }
  169. totalHits := 200
  170. result := calculateTopFieldStats(facet, totalHits, func(term string, count int, percent float64) URLAccessStats {
  171. return URLAccessStats{URL: term, Visits: count, Percent: percent}
  172. })
  173. assert.NotNil(t, result)
  174. assert.Len(t, result, 3)
  175. // Check first item
  176. assert.Equal(t, "/api/users", result[0].URL)
  177. assert.Equal(t, 100, result[0].Visits)
  178. assert.Equal(t, 50.0, result[0].Percent) // 100/200 * 100
  179. // Check second item
  180. assert.Equal(t, "/api/posts", result[1].URL)
  181. assert.Equal(t, 50, result[1].Visits)
  182. assert.Equal(t, 25.0, result[1].Percent) // 50/200 * 100
  183. }
  184. func TestCalculateTopFieldStats_EmptyFacet(t *testing.T) {
  185. result := calculateTopFieldStats[URLAccessStats](nil, 100, func(term string, count int, percent float64) URLAccessStats {
  186. return URLAccessStats{URL: term, Visits: count, Percent: percent}
  187. })
  188. assert.NotNil(t, result)
  189. assert.Len(t, result, 0)
  190. }
  191. func TestCalculateTopFieldStats_ZeroHits(t *testing.T) {
  192. facet := &searcher.Facet{
  193. Terms: []*searcher.FacetTerm{
  194. {Term: "/api/users", Count: 100},
  195. },
  196. }
  197. result := calculateTopFieldStats(facet, 0, func(term string, count int, percent float64) URLAccessStats {
  198. return URLAccessStats{URL: term, Visits: count, Percent: percent}
  199. })
  200. assert.NotNil(t, result)
  201. assert.Len(t, result, 0)
  202. }
  203. func TestService_ValidateTimeRange_Comprehensive(t *testing.T) {
  204. mockSearcher := &MockSearcher{}
  205. s := NewService(mockSearcher)
  206. tests := []struct {
  207. name string
  208. startTime int64
  209. endTime int64
  210. expected bool
  211. }{
  212. {"valid range", 1000, 2000, true},
  213. {"invalid range - same", 1000, 1000, false},
  214. {"invalid range - backwards", 2000, 1000, false},
  215. {"zero range", 0, 0, true},
  216. {"negative start", -1000, 2000, false},
  217. {"negative end", 1000, -2000, false},
  218. }
  219. for _, tt := range tests {
  220. t.Run(tt.name, func(t *testing.T) {
  221. err := s.ValidateTimeRange(tt.startTime, tt.endTime)
  222. if tt.expected {
  223. assert.NoError(t, err)
  224. } else {
  225. assert.Error(t, err)
  226. }
  227. })
  228. }
  229. }
  230. func TestGetTopKeyValuesFromMap(t *testing.T) {
  231. counts := map[string]int{
  232. "200": 2,
  233. "404": 1,
  234. "500": 3,
  235. }
  236. result := getTopKeyValuesFromMap(counts, 10) // Set a reasonable limit
  237. assert.NotNil(t, result)
  238. assert.Len(t, result, 3)
  239. // Should be sorted by value descending
  240. assert.Equal(t, "500", result[0].Key)
  241. assert.Equal(t, 3, result[0].Value)
  242. assert.Equal(t, "200", result[1].Key)
  243. assert.Equal(t, 2, result[1].Value)
  244. assert.Equal(t, "404", result[2].Key)
  245. assert.Equal(t, 1, result[2].Value)
  246. }
  247. func TestGetTopKeyValuesFromMap_WithLimit(t *testing.T) {
  248. counts := map[string]int{
  249. "a": 1,
  250. "b": 2,
  251. "c": 3,
  252. }
  253. result := getTopKeyValuesFromMap(counts, 2)
  254. assert.NotNil(t, result)
  255. assert.Len(t, result, 2) // Should be limited to 2
  256. // Should be sorted by value descending
  257. assert.Equal(t, "c", result[0].Key)
  258. assert.Equal(t, 3, result[0].Value)
  259. assert.Equal(t, "b", result[1].Key)
  260. assert.Equal(t, 2, result[1].Value)
  261. }
  262. func TestService_calculateBrowserStats_FromFacets(t *testing.T) {
  263. result := &searcher.SearchResult{
  264. TotalHits: 1000,
  265. Facets: map[string]*searcher.Facet{
  266. "browser": {
  267. Terms: []*searcher.FacetTerm{
  268. {Term: "Chrome", Count: 600},
  269. {Term: "Firefox", Count: 300},
  270. {Term: "Safari", Count: 100},
  271. },
  272. },
  273. },
  274. }
  275. mockSearcher := &MockSearcher{}
  276. s := NewService(mockSearcher).(*service)
  277. stats := s.calculateBrowserStats(result)
  278. assert.NotNil(t, stats)
  279. assert.Len(t, stats, 3)
  280. // Check sorting and calculations
  281. assert.Equal(t, "Chrome", stats[0].Browser)
  282. assert.Equal(t, 600, stats[0].Count)
  283. assert.Equal(t, 60.0, stats[0].Percent) // 600/1000 * 100
  284. assert.Equal(t, "Firefox", stats[1].Browser)
  285. assert.Equal(t, 300, stats[1].Count)
  286. assert.Equal(t, 30.0, stats[1].Percent) // 300/1000 * 100
  287. }
  288. func TestService_calculateOSStats_FromFacets(t *testing.T) {
  289. result := &searcher.SearchResult{
  290. TotalHits: 800,
  291. Facets: map[string]*searcher.Facet{
  292. "os": {
  293. Terms: []*searcher.FacetTerm{
  294. {Term: "Windows", Count: 400},
  295. {Term: "macOS", Count: 250},
  296. {Term: "Linux", Count: 150},
  297. },
  298. },
  299. },
  300. }
  301. mockSearcher := &MockSearcher{}
  302. s := NewService(mockSearcher).(*service)
  303. stats := s.calculateOSStats(result)
  304. assert.NotNil(t, stats)
  305. assert.Len(t, stats, 3)
  306. // Check sorting and calculations
  307. assert.Equal(t, "Windows", stats[0].OS)
  308. assert.Equal(t, 400, stats[0].Count)
  309. assert.Equal(t, 50.0, stats[0].Percent) // 400/800 * 100
  310. assert.Equal(t, "macOS", stats[1].OS)
  311. assert.Equal(t, 250, stats[1].Count)
  312. assert.Equal(t, 31.25, stats[1].Percent) // 250/800 * 100
  313. }
  314. func TestService_GetVisitorsByCountry_Success(t *testing.T) {
  315. mockSearcher := &MockSearcher{}
  316. s := NewService(mockSearcher).(*service)
  317. ctx := context.Background()
  318. req := &VisitorsByCountryRequest{
  319. StartTime: 1000,
  320. EndTime: 2000,
  321. LogPaths: []string{"/var/log/nginx/access.log"},
  322. }
  323. expectedResult := &searcher.SearchResult{
  324. Facets: map[string]*searcher.Facet{
  325. "region_code": {
  326. Terms: []*searcher.FacetTerm{
  327. {Term: "US", Count: 100},
  328. {Term: "CN", Count: 50},
  329. },
  330. },
  331. },
  332. }
  333. mockSearcher.On("Search", ctx, mock.AnythingOfType("*searcher.SearchRequest")).Return(expectedResult, nil)
  334. result, err := s.GetVisitorsByCountry(ctx, req)
  335. assert.NoError(t, err)
  336. assert.NotNil(t, result)
  337. assert.Equal(t, 100, result.Data["US"])
  338. assert.Equal(t, 50, result.Data["CN"])
  339. }
  340. func TestService_GetErrorDistribution_Success(t *testing.T) {
  341. mockSearcher := &MockSearcher{}
  342. s := NewService(mockSearcher).(*service)
  343. ctx := context.Background()
  344. req := &ErrorDistributionRequest{
  345. StartTime: 1000,
  346. EndTime: 2000,
  347. LogPaths: []string{"/var/log/nginx/access.log"},
  348. }
  349. expectedResult := &searcher.SearchResult{
  350. Facets: map[string]*searcher.Facet{
  351. "status": {
  352. Terms: []*searcher.FacetTerm{
  353. {Term: "404", Count: 20},
  354. {Term: "500", Count: 5},
  355. },
  356. },
  357. },
  358. }
  359. mockSearcher.On("Search", ctx, mock.MatchedBy(func(r *searcher.SearchRequest) bool {
  360. return r.Query == "status:[400 TO 599]"
  361. })).Return(expectedResult, nil)
  362. result, err := s.GetErrorDistribution(ctx, req)
  363. assert.NoError(t, err)
  364. assert.NotNil(t, result)
  365. assert.Equal(t, 20, result.Data["404"])
  366. assert.Equal(t, 5, result.Data["500"])
  367. }