123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- package analytics
- import (
- "context"
- "testing"
- "github.com/0xJacky/Nginx-UI/internal/nginx_log/searcher"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- )
- func TestService_GetDashboardAnalytics_HourlyStats(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping test in short mode")
- }
- mockSearcher := &MockSearcher{}
- s := NewService(mockSearcher)
- ctx := context.Background()
- req := &DashboardQueryRequest{
- StartTime: 1640995200, // 2022-01-01 00:00:00 UTC
- EndTime: 1641006000, // 2022-01-01 03:00:00 UTC (same day as test data)
- LogPaths: []string{"/var/log/nginx/access.log"},
- }
- expectedResult := &searcher.SearchResult{
- TotalHits: 3,
- Hits: []*searcher.SearchHit{
- {
- Fields: map[string]interface{}{
- "timestamp": float64(1640995800), // 2022-01-01 00:10:00
- "ip": "192.168.1.1",
- "bytes": int64(1024),
- },
- },
- {
- Fields: map[string]interface{}{
- "timestamp": float64(1640999400), // 2022-01-01 01:10:00
- "ip": "192.168.1.2",
- "bytes": int64(2048),
- },
- },
- {
- Fields: map[string]interface{}{
- "timestamp": float64(1640999500), // 2022-01-01 01:11:40
- "ip": "192.168.1.1", // Same IP as first hit
- "bytes": int64(512),
- },
- },
- },
- Facets: map[string]*searcher.Facet{
- "ip": {
- Terms: []*searcher.FacetTerm{
- {Term: "192.168.1.1", Count: 2},
- {Term: "192.168.1.2", Count: 1},
- },
- },
- },
- }
- mockSearcher.On("Search", ctx, mock.AnythingOfType("*searcher.SearchRequest")).Return(expectedResult, nil)
- result, err := s.GetDashboardAnalytics(ctx, req)
- assert.NoError(t, err)
- assert.NotNil(t, result)
- assert.NotEmpty(t, result.HourlyStats)
- // Check that we have some hourly data - the specific hours depend on the test data timestamps
- var totalPV, totalUV int
- for _, stat := range result.HourlyStats {
- totalPV += stat.PV
- totalUV += stat.UV
- }
- // We should have some aggregated data
- assert.Greater(t, totalPV, 0)
- assert.Greater(t, totalUV, 0)
- mockSearcher.AssertExpectations(t)
- }
- // Duplicate test functions removed - they exist in dashboard_test.go
- func TestService_calculateHourlyStats_HourlyInterval(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping test in short mode")
- }
- mockSearcher := &MockSearcher{}
- s := NewService(mockSearcher).(*service)
- result := &searcher.SearchResult{
- TotalHits: 2,
- Hits: []*searcher.SearchHit{
- {
- Fields: map[string]interface{}{
- "timestamp": float64(1640995800), // 2022-01-01 00:10:00 UTC
- "ip": "192.168.1.1",
- },
- },
- {
- Fields: map[string]interface{}{
- "timestamp": float64(1641002400), // 2022-01-01 02:00:00 UTC
- "ip": "192.168.1.2",
- },
- },
- },
- }
- startTime := int64(1640995200) // 2022-01-01 00:00:00 UTC
- endTime := int64(1641002400) // 2022-01-01 02:00:00 UTC
- stats := s.calculateHourlyStats(result, startTime, endTime)
- assert.NotNil(t, stats)
- assert.GreaterOrEqual(t, len(stats), 2) // Should have at least 2 hours
- // Check that stats are sorted by timestamp (not just hour, since we have 48 hours of data)
- for i := 1; i < len(stats); i++ {
- assert.LessOrEqual(t, stats[i-1].Timestamp, stats[i].Timestamp)
- }
- }
- func TestService_calculateDailyStats_DailyInterval(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping test in short mode")
- }
- mockSearcher := &MockSearcher{}
- s := NewService(mockSearcher).(*service)
- result := &searcher.SearchResult{
- TotalHits: 2,
- Hits: []*searcher.SearchHit{
- {
- Fields: map[string]interface{}{
- "timestamp": float64(1640995800), // 2022-01-01 00:10:00 UTC
- "ip": "192.168.1.1",
- },
- },
- {
- Fields: map[string]interface{}{
- "timestamp": float64(1641168000), // 2022-01-03 00:00:00 UTC
- "ip": "192.168.1.2",
- },
- },
- },
- }
- startTime := int64(1640995200) // 2022-01-01 00:00:00 UTC
- endTime := int64(1641168000) // 2022-01-03 00:00:00 UTC
- stats := s.calculateDailyStats(result, startTime, endTime)
- assert.NotNil(t, stats)
- assert.GreaterOrEqual(t, len(stats), 2) // Should have at least 2 days
- // Check that stats are sorted by timestamp
- for i := 1; i < len(stats); i++ {
- assert.LessOrEqual(t, stats[i-1].Timestamp, stats[i].Timestamp)
- }
- }
- func TestService_calculateDashboardSummary_MonthlyData(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping test in short mode")
- }
- mockSearcher := &MockSearcher{}
- s := NewService(mockSearcher).(*service)
- analytics := &DashboardAnalytics{
- HourlyStats: []HourlyAccessStats{
- {Hour: 0, UV: 10, PV: 100},
- {Hour: 1, UV: 20, PV: 200},
- {Hour: 2, UV: 15, PV: 150},
- },
- DailyStats: []DailyAccessStats{
- {Date: "2022-01-01", UV: 30, PV: 300, Timestamp: 1640995200},
- {Date: "2022-01-02", UV: 25, PV: 250, Timestamp: 1641081600},
- {Date: "2022-01-03", UV: 28, PV: 280, Timestamp: 1641168000},
- },
- }
- result := &searcher.SearchResult{
- TotalHits: 830,
- Facets: map[string]*searcher.Facet{
- "ip": {
- Total: 50, // 50 unique IPs
- },
- },
- }
- summary := s.calculateDashboardSummary(analytics, result)
- assert.Equal(t, 50, summary.TotalUV)
- assert.Equal(t, 830, summary.TotalPV)
- assert.InDelta(t, 16.67, summary.AvgDailyUV, 0.01) // 50 total UV / 3 days
- assert.InDelta(t, 276.67, summary.AvgDailyPV, 0.01) // (300+250+280)/3
- // Peak hour should be hour 1 with 200 PV
- assert.Equal(t, 1, summary.PeakHour)
- assert.Equal(t, 200, summary.PeakHourTraffic)
- }
- func TestService_calculateTopFieldStats_Generic(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping test in short mode")
- }
- facet := &searcher.Facet{
- Terms: []*searcher.FacetTerm{
- {Term: "/api/users", Count: 100},
- {Term: "/api/posts", Count: 50},
- {Term: "/", Count: 25},
- },
- }
- totalHits := 200
- result := calculateTopFieldStats(facet, totalHits, func(term string, count int, percent float64) URLAccessStats {
- return URLAccessStats{URL: term, Visits: count, Percent: percent}
- })
- assert.NotNil(t, result)
- assert.Len(t, result, 3)
- // Check first item
- assert.Equal(t, "/api/users", result[0].URL)
- assert.Equal(t, 100, result[0].Visits)
- assert.Equal(t, 50.0, result[0].Percent) // 100/200 * 100
- // Check second item
- assert.Equal(t, "/api/posts", result[1].URL)
- assert.Equal(t, 50, result[1].Visits)
- assert.Equal(t, 25.0, result[1].Percent) // 50/200 * 100
- }
- func TestCalculateTopFieldStats_EmptyFacet(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping test in short mode")
- }
- result := calculateTopFieldStats[URLAccessStats](nil, 100, func(term string, count int, percent float64) URLAccessStats {
- return URLAccessStats{URL: term, Visits: count, Percent: percent}
- })
- assert.NotNil(t, result)
- assert.Len(t, result, 0)
- }
- func TestCalculateTopFieldStats_ZeroHits(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping test in short mode")
- }
- facet := &searcher.Facet{
- Terms: []*searcher.FacetTerm{
- {Term: "/api/users", Count: 100},
- },
- }
- result := calculateTopFieldStats(facet, 0, func(term string, count int, percent float64) URLAccessStats {
- return URLAccessStats{URL: term, Visits: count, Percent: percent}
- })
- assert.NotNil(t, result)
- assert.Len(t, result, 0)
- }
- func TestService_ValidateTimeRange_Comprehensive(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping test in short mode")
- }
- mockSearcher := &MockSearcher{}
- s := NewService(mockSearcher)
- tests := []struct {
- name string
- startTime int64
- endTime int64
- expected bool
- }{
- {"valid range", 1000, 2000, true},
- {"invalid range - same", 1000, 1000, false},
- {"invalid range - backwards", 2000, 1000, false},
- {"zero range", 0, 0, true},
- {"negative start", -1000, 2000, false},
- {"negative end", 1000, -2000, false},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- err := s.ValidateTimeRange(tt.startTime, tt.endTime)
- if tt.expected {
- assert.NoError(t, err)
- } else {
- assert.Error(t, err)
- }
- })
- }
- }
- func TestGetTopKeyValuesFromMap(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping test in short mode")
- }
- counts := map[string]int{
- "200": 2,
- "404": 1,
- "500": 3,
- }
- result := getTopKeyValuesFromMap(counts, 10) // Set a reasonable limit
- assert.NotNil(t, result)
- assert.Len(t, result, 3)
- // Should be sorted by value descending
- assert.Equal(t, "500", result[0].Key)
- assert.Equal(t, 3, result[0].Value)
- assert.Equal(t, "200", result[1].Key)
- assert.Equal(t, 2, result[1].Value)
- assert.Equal(t, "404", result[2].Key)
- assert.Equal(t, 1, result[2].Value)
- }
- func TestGetTopKeyValuesFromMap_WithLimit(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping test in short mode")
- }
- counts := map[string]int{
- "a": 1,
- "b": 2,
- "c": 3,
- }
- result := getTopKeyValuesFromMap(counts, 2)
- assert.NotNil(t, result)
- assert.Len(t, result, 2) // Should be limited to 2
- // Should be sorted by value descending
- assert.Equal(t, "c", result[0].Key)
- assert.Equal(t, 3, result[0].Value)
- assert.Equal(t, "b", result[1].Key)
- assert.Equal(t, 2, result[1].Value)
- }
- func TestService_calculateBrowserStats_FromFacets(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping test in short mode")
- }
- result := &searcher.SearchResult{
- TotalHits: 1000,
- Facets: map[string]*searcher.Facet{
- "browser": {
- Terms: []*searcher.FacetTerm{
- {Term: "Chrome", Count: 600},
- {Term: "Firefox", Count: 300},
- {Term: "Safari", Count: 100},
- },
- },
- },
- }
- mockSearcher := &MockSearcher{}
- s := NewService(mockSearcher).(*service)
- stats := s.calculateBrowserStats(result)
- assert.NotNil(t, stats)
- assert.Len(t, stats, 3)
- // Check sorting and calculations
- assert.Equal(t, "Chrome", stats[0].Browser)
- assert.Equal(t, 600, stats[0].Count)
- assert.Equal(t, 60.0, stats[0].Percent) // 600/1000 * 100
- assert.Equal(t, "Firefox", stats[1].Browser)
- assert.Equal(t, 300, stats[1].Count)
- assert.Equal(t, 30.0, stats[1].Percent) // 300/1000 * 100
- }
- func TestService_calculateOSStats_FromFacets(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping test in short mode")
- }
- result := &searcher.SearchResult{
- TotalHits: 800,
- Facets: map[string]*searcher.Facet{
- "os": {
- Terms: []*searcher.FacetTerm{
- {Term: "Windows", Count: 400},
- {Term: "macOS", Count: 250},
- {Term: "Linux", Count: 150},
- },
- },
- },
- }
- mockSearcher := &MockSearcher{}
- s := NewService(mockSearcher).(*service)
- stats := s.calculateOSStats(result)
- assert.NotNil(t, stats)
- assert.Len(t, stats, 3)
- // Check sorting and calculations
- assert.Equal(t, "Windows", stats[0].OS)
- assert.Equal(t, 400, stats[0].Count)
- assert.Equal(t, 50.0, stats[0].Percent) // 400/800 * 100
- assert.Equal(t, "macOS", stats[1].OS)
- assert.Equal(t, 250, stats[1].Count)
- assert.Equal(t, 31.25, stats[1].Percent) // 250/800 * 100
- }
- func TestService_GetVisitorsByCountry_Success(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping test in short mode")
- }
- mockSearcher := &MockSearcher{}
- s := NewService(mockSearcher).(*service)
- ctx := context.Background()
- req := &VisitorsByCountryRequest{
- StartTime: 1000,
- EndTime: 2000,
- LogPaths: []string{"/var/log/nginx/access.log"},
- }
- expectedResult := &searcher.SearchResult{
- Facets: map[string]*searcher.Facet{
- "region_code": {
- Terms: []*searcher.FacetTerm{
- {Term: "US", Count: 100},
- {Term: "CN", Count: 50},
- },
- },
- },
- }
- mockSearcher.On("Search", ctx, mock.AnythingOfType("*searcher.SearchRequest")).Return(expectedResult, nil)
- result, err := s.GetVisitorsByCountry(ctx, req)
- assert.NoError(t, err)
- assert.NotNil(t, result)
- assert.Equal(t, 100, result.Data["US"])
- assert.Equal(t, 50, result.Data["CN"])
- }
- func TestService_GetErrorDistribution_Success(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping test in short mode")
- }
- mockSearcher := &MockSearcher{}
- s := NewService(mockSearcher).(*service)
- ctx := context.Background()
- req := &ErrorDistributionRequest{
- StartTime: 1000,
- EndTime: 2000,
- LogPaths: []string{"/var/log/nginx/access.log"},
- }
- expectedResult := &searcher.SearchResult{
- Facets: map[string]*searcher.Facet{
- "status": {
- Terms: []*searcher.FacetTerm{
- {Term: "404", Count: 20},
- {Term: "500", Count: 5},
- },
- },
- },
- }
- mockSearcher.On("Search", ctx, mock.MatchedBy(func(r *searcher.SearchRequest) bool {
- return r.Query == "status:[400 TO 599]"
- })).Return(expectedResult, nil)
- result, err := s.GetErrorDistribution(ctx, req)
- assert.NoError(t, err)
- assert.NotNil(t, result)
- assert.Equal(t, 20, result.Data["404"])
- assert.Equal(t, 5, result.Data["500"])
- }
|