| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 | package analyticsimport (	"context"	"testing"	"github.com/0xJacky/Nginx-UI/internal/nginx_log/searcher"	"github.com/stretchr/testify/assert"	"github.com/stretchr/testify/mock")func TestService_GetDashboardAnalytics_Success(t *testing.T) {	mockSearcher := &MockSearcher{}	s := NewService(mockSearcher)	ctx := context.Background()	req := &DashboardQueryRequest{		StartTime: 1640995200, // 2022-01-01 00:00:00 UTC		EndTime:   1641081600, // 2022-01-02 00:00:00 UTC		LogPath:   "/var/log/nginx/access.log",	}	// Mock search result with more comprehensive sample data for in-memory calculation	expectedResult := &searcher.SearchResult{		TotalHits: 4,		Hits: []*searcher.SearchHit{			{				Fields: map[string]interface{}{					"timestamp":   float64(1640995800), // 2022-01-01 00:10:00 UTC (hour 0)					"ip":          "192.168.1.1",					"path":        "/api/users",					"browser":     "Chrome",					"os":          "Windows",					"device_type": "Desktop",				},			},			{				Fields: map[string]interface{}{					"timestamp":   float64(1640999400), // 2022-01-01 01:10:00 UTC (hour 1)					"ip":          "192.168.1.2",					"path":        "/api/posts",					"browser":     "Firefox",					"os":          "Linux",					"device_type": "Desktop",				},			},			{				Fields: map[string]interface{}{					"timestamp":   float64(1640999500), // 2022-01-01 01:11:40 UTC (hour 1)					"ip":          "192.168.1.1",					"path":        "/api/users",					"browser":     "Chrome",					"os":          "Windows",					"device_type": "Mobile",				},			},			{				Fields: map[string]interface{}{					"timestamp":   float64(1641082200), // 2022-01-02 00:10:00 UTC (day 2)					"ip":          "192.168.1.3",					"path":        "/",					"browser":     "Chrome",					"os":          "macOS",					"device_type": "Desktop",				},			},		},		Facets: map[string]*searcher.Facet{			"path_exact": {				Terms: []*searcher.FacetTerm{					{Term: "/api/users", Count: 2},					{Term: "/api/posts", Count: 1},					{Term: "/", Count: 1},				},			},			"browser": {				Terms: []*searcher.FacetTerm{					{Term: "Chrome", Count: 3},					{Term: "Firefox", Count: 1},				},			},			"os": {				Terms: []*searcher.FacetTerm{					{Term: "Windows", Count: 2},					{Term: "Linux", Count: 1},					{Term: "macOS", Count: 1},				},			},			"device_type": {				Terms: []*searcher.FacetTerm{					{Term: "Desktop", Count: 3},					{Term: "Mobile", Count: 1},				},			},			"ip": {				Total: 3, // 3 unique IPs				Terms: []*searcher.FacetTerm{					{Term: "192.168.1.1", Count: 2},					{Term: "192.168.1.2", Count: 1},					{Term: "192.168.1.3", 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)	// Check that all arrays are initialized	assert.NotNil(t, result.HourlyStats)	assert.NotNil(t, result.DailyStats)	assert.NotNil(t, result.TopURLs)	assert.NotNil(t, result.Browsers)	assert.NotNil(t, result.OperatingSystems)	assert.NotNil(t, result.Devices)	// Check TopURLs (calculated from hits)	assert.Len(t, result.TopURLs, 3)	assert.Equal(t, "/api/users", result.TopURLs[0].URL)	assert.Equal(t, 2, result.TopURLs[0].Visits)	// Check Browsers (calculated from hits)	assert.Len(t, result.Browsers, 2)	assert.Equal(t, "Chrome", result.Browsers[0].Browser)	assert.Equal(t, 3, result.Browsers[0].Count)	// Check Devices (calculated from hits)	assert.Len(t, result.Devices, 2)	assert.Equal(t, "Desktop", result.Devices[0].Device)	assert.Equal(t, 3, result.Devices[0].Count)	// Check Summary	assert.Equal(t, 3, result.Summary.TotalUV) // 3 unique IPs	assert.Equal(t, 4, result.Summary.TotalPV)	mockSearcher.AssertExpectations(t)}func TestService_GetDashboardAnalytics_NilRequest(t *testing.T) {	mockSearcher := &MockSearcher{}	s := NewService(mockSearcher)	ctx := context.Background()	result, err := s.GetDashboardAnalytics(ctx, nil)	assert.Error(t, err)	assert.Nil(t, result)	assert.Contains(t, err.Error(), "request cannot be nil")}func TestService_GetDashboardAnalytics_InvalidTimeRange(t *testing.T) {	mockSearcher := &MockSearcher{}	s := NewService(mockSearcher)	ctx := context.Background()	req := &DashboardQueryRequest{		StartTime: 2000,		EndTime:   1000, // End before start		LogPath:   "/var/log/nginx/access.log",	}	result, err := s.GetDashboardAnalytics(ctx, req)	assert.Error(t, err)	assert.Nil(t, result)	assert.Contains(t, err.Error(), "invalid time range")}func TestService_GetDashboardAnalytics_SearchError(t *testing.T) {	mockSearcher := &MockSearcher{}	s := NewService(mockSearcher)	ctx := context.Background()	req := &DashboardQueryRequest{		StartTime: 1000,		EndTime:   2000,		LogPath:   "/var/log/nginx/access.log",	}	mockSearcher.On("Search", ctx, mock.AnythingOfType("*searcher.SearchRequest")).Return(nil, assert.AnError)	result, err := s.GetDashboardAnalytics(ctx, req)	assert.Error(t, err)	assert.Nil(t, result)	assert.Contains(t, err.Error(), "failed to search logs")	mockSearcher.AssertExpectations(t)}func TestService_GetDashboardAnalytics_EmptyResult(t *testing.T) {	mockSearcher := &MockSearcher{}	s := NewService(mockSearcher)	ctx := context.Background()	req := &DashboardQueryRequest{		StartTime: 1000,		EndTime:   2000,		LogPath:   "/var/log/nginx/access.log",	}	// Empty search result	expectedResult := &searcher.SearchResult{		TotalHits: 0,		Hits:      []*searcher.SearchHit{},	}	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)		// All arrays should not be nil, but can be empty	assert.NotNil(t, result.HourlyStats)	assert.NotNil(t, result.DailyStats)	assert.Len(t, result.TopURLs, 0)	assert.Len(t, result.Browsers, 0)	assert.Len(t, result.OperatingSystems, 0)	assert.Len(t, result.Devices, 0)	// Summary should have zero values	assert.Equal(t, 0, result.Summary.TotalUV)	assert.Equal(t, 0, result.Summary.TotalPV)	mockSearcher.AssertExpectations(t)}func TestService_calculateHourlyStats(t *testing.T) {	mockSearcher := &MockSearcher{}	s := NewService(mockSearcher).(*service)	// Create test data spanning multiple hours	result := &searcher.SearchResult{		TotalHits: 3,		Hits: []*searcher.SearchHit{			{				Fields: map[string]interface{}{					"timestamp": float64(1640995800), // 2022-01-01 00:10:00 UTC (hour 0)					"ip":        "192.168.1.1",				},			},			{				Fields: map[string]interface{}{					"timestamp": float64(1640999400), // 2022-01-01 01:10:00 UTC (hour 1)					"ip":        "192.168.1.2",				},			},			{				Fields: map[string]interface{}{					"timestamp": float64(1640999500), // 2022-01-01 01:11:40 UTC (hour 1)					"ip":        "192.168.1.1", // Same IP as first hit				},			},		},	}	startTime := int64(1640995200) // 2022-01-01 00:00:00 UTC	endTime := int64(1641006000)   // 2022-01-01 03:00:00 UTC (extended range)	stats := s.calculateHourlyStats(result, startTime, endTime)	assert.NotNil(t, stats)	assert.GreaterOrEqual(t, len(stats), 3) // Should have at least 3 hours	// Find stats with actual data (non-zero PV)	var statsWithData []*HourlyAccessStats	for i := range stats {		if stats[i].PV > 0 {			statsWithData = append(statsWithData, &stats[i])		}	}	assert.Len(t, statsWithData, 2) // Should have 2 hours with data	// First hour with data should have 1 PV and 1 UV	assert.Equal(t, 1, statsWithData[0].PV)	assert.Equal(t, 1, statsWithData[0].UV)	// Second hour with data should have 2 PV and 2 UV  	assert.Equal(t, 2, statsWithData[1].PV)	assert.Equal(t, 2, statsWithData[1].UV)}func TestService_calculateDailyStats(t *testing.T) {	mockSearcher := &MockSearcher{}	s := NewService(mockSearcher).(*service)	// Create test data spanning multiple days	result := &searcher.SearchResult{		TotalHits: 3,		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(1641082200), // 2022-01-02 00:10:00 UTC					"ip":        "192.168.1.2",				},			},			{				Fields: map[string]interface{}{					"timestamp": float64(1641082800), // 2022-01-02 00:20:00 UTC					"ip":        "192.168.1.1", // Same IP as first hit				},			},		},	}	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.Len(t, stats, 3) // Should have 3 days because we initialize for the full range	// Verify stats are sorted by timestamp	for i := 1; i < len(stats); i++ {		assert.LessOrEqual(t, stats[i-1].Timestamp, stats[i].Timestamp)	}	// Find the days with data	var day1Stats, day2Stats *DailyAccessStats	for i := range stats {		if stats[i].Date == "2022-01-01" {			day1Stats = &stats[i]		} else if stats[i].Date == "2022-01-02" {			day2Stats = &stats[i]		}	}	assert.NotNil(t, day1Stats)	assert.NotNil(t, day2Stats)	// Day 1 should have 1 PV and 1 UV	assert.Equal(t, 1, day1Stats.PV)	assert.Equal(t, 1, day1Stats.UV)	// Day 2 should have 2 PV and 2 UV	assert.Equal(t, 2, day2Stats.PV)	assert.Equal(t, 2, day2Stats.UV)}// Test for the generic top field stats calculatorfunc Test_calculateTopFieldStats(t *testing.T) {	facet := &searcher.Facet{		Terms: []*searcher.FacetTerm{			{Term: "/a", Count: 3},			{Term: "/b", Count: 2},			{Term: "/c", Count: 1},		},	}	stats := calculateTopFieldStats(facet, 6, func(term string, count int, percent float64) URLAccessStats {		return URLAccessStats{URL: term, Visits: count, Percent: percent}	})	assert.NotNil(t, stats)	assert.Len(t, stats, 3) // Should have all 3 terms from facet	// Should be sorted by visits descending	assert.Equal(t, "/a", stats[0].URL)	assert.Equal(t, 3, stats[0].Visits)	assert.InDelta(t, 50.0, stats[0].Percent, 0.01) // 3/6	assert.Equal(t, "/b", stats[1].URL)	assert.Equal(t, 2, stats[1].Visits)	assert.InDelta(t, 33.33, stats[1].Percent, 0.01) // 2/6}func TestService_calculateDashboardSummary(t *testing.T) {	mockSearcher := &MockSearcher{}	s := NewService(mockSearcher).(*service)	analytics := &DashboardAnalytics{		HourlyStats: []HourlyAccessStats{			{Hour: 0, UV: 10, PV: 100},			{Hour: 1, UV: 20, PV: 200}, // Peak hour			{Hour: 2, UV: 15, PV: 150},		},		DailyStats: []DailyAccessStats{			{Date: "2022-01-01", UV: 30, PV: 300},			{Date: "2022-01-02", UV: 25, PV: 250},		},	}	result := &searcher.SearchResult{		TotalHits: 550,		Hits: []*searcher.SearchHit{			{Fields: map[string]interface{}{"ip": "192.168.1.1"}},			{Fields: map[string]interface{}{"ip": "192.168.1.2"}},			{Fields: map[string]interface{}{"ip": "192.168.1.1"}}, // Duplicate		},		Facets: map[string]*searcher.Facet{			"ip": {				Total: 2, // 2 unique IPs				Terms: []*searcher.FacetTerm{					{Term: "192.168.1.1", Count: 2},					{Term: "192.168.1.2", Count: 1},				},			},		},	}	summary := s.calculateDashboardSummary(analytics, result)	assert.Equal(t, 2, summary.TotalUV)   // 2 unique IPs from hits	assert.Equal(t, 550, summary.TotalPV) // Total hits from result	// Average daily values (2 days)	assert.InDelta(t, 1.0, summary.AvgDailyUV, 0.01) // 2 total UV / 2 days = 1	assert.InDelta(t, 275.0, summary.AvgDailyPV, 0.01) // (300 + 250) / 2	// Peak hour should be hour 1 with 200 PV	assert.Equal(t, 1, summary.PeakHour)	assert.Equal(t, 200, summary.PeakHourTraffic)}
 |