// SPDX-FileCopyrightText: 2022 Free Mobile // SPDX-License-Identifier: AGPL-3.0-only package console import ( "net" "reflect" "strings" "testing" "time" "github.com/ClickHouse/clickhouse-go/v2/lib/driver" "github.com/gin-gonic/gin" "go.uber.org/mock/gomock" "akvorado/common/clickhousedb/mocks" "akvorado/common/helpers" ) func TestWidgetLastFlow(t *testing.T) { _, h, mockConn, _ := NewMock(t, DefaultConfiguration()) ctrl := gomock.NewController(t) mockRows := mocks.NewMockRows(ctrl) mockConn.EXPECT().Query(gomock.Any(), ` SELECT * EXCEPT (DstCommunities, DstLargeCommunities), arrayMap(c -> concat(toString(bitShiftRight(c, 16)), ':', toString(bitAnd(c, 0xffff))), DstCommunities) AS DstCommunities, arrayMap(c -> concat(toString(bitAnd(bitShiftRight(c, 64), 0xffffffff)), ':', toString(bitAnd(bitShiftRight(c, 32), 0xffffffff)), ':', toString(bitAnd(c, 0xffffffff))), DstLargeCommunities) AS DstLargeCommunities FROM flows WHERE TimeReceived=(SELECT MAX(TimeReceived) FROM flows) LIMIT 1`). Return(mockRows, nil) mockRows.EXPECT().Next().Return(true) mockRows.EXPECT().Close() mockRows.EXPECT().Columns().Return([]string{ "TimeReceived", "SamplingRate", "SrcAddr", "SrcCountry", "InIfName", "InIfBoundary", "InIfSpeed", }).AnyTimes() colTimeReceived := mocks.NewMockColumnType(ctrl) colSamplingRate := mocks.NewMockColumnType(ctrl) colSrcAddr := mocks.NewMockColumnType(ctrl) colSrcCountry := mocks.NewMockColumnType(ctrl) colInIfName := mocks.NewMockColumnType(ctrl) colInIfBoundary := mocks.NewMockColumnType(ctrl) colInIfSpeed := mocks.NewMockColumnType(ctrl) colTimeReceived.EXPECT().ScanType().Return(reflect.TypeFor[time.Time]()) colSamplingRate.EXPECT().ScanType().Return(reflect.TypeFor[uint64]()) colSrcAddr.EXPECT().ScanType().Return(reflect.TypeFor[net.IP]()) colSrcCountry.EXPECT().ScanType().Return(reflect.TypeFor[string]()) colInIfName.EXPECT().ScanType().Return(reflect.TypeFor[string]()) colInIfBoundary.EXPECT().ScanType().Return(reflect.TypeFor[string]()) colInIfSpeed.EXPECT().ScanType().Return(reflect.TypeFor[uint32]()) mockRows.EXPECT().ColumnTypes().Return([]driver.ColumnType{ colTimeReceived, colSamplingRate, colSrcAddr, colSrcCountry, colInIfName, colInIfBoundary, colInIfSpeed, }).AnyTimes() mockRows.EXPECT().Scan(gomock.Any()). DoAndReturn(func(args ...any) any { arg0 := args[0].(*time.Time) *arg0 = time.Date(2022, 4, 4, 8, 36, 11, 10, time.UTC) arg1 := args[1].(*uint64) *arg1 = uint64(10000) arg2 := args[2].(*net.IP) *arg2 = net.ParseIP("2001:db8::22") arg3 := args[3].(*string) *arg3 = "FR" arg4 := args[4].(*string) *arg4 = "Hu0/0/1/10" arg5 := args[5].(*string) *arg5 = "external" arg6 := args[6].(*uint32) *arg6 = uint32(100000) return nil }) helpers.TestHTTPEndpoints(t, h.LocalAddr(), helpers.HTTPEndpointCases{ { URL: "/api/v0/console/widget/flow-last", JSONOutput: gin.H{ "InIfBoundary": "external", "InIfName": "Hu0/0/1/10", "InIfSpeed": 100000, "SamplingRate": 10000, "SrcAddr": "2001:db8::22", "SrcCountry": "FR", "TimeReceived": "2022-04-04T08:36:11.00000001Z", }, }, }) } func TestFlowRate(t *testing.T) { _, h, mockConn, _ := NewMock(t, DefaultConfiguration()) ctrl := gomock.NewController(t) mockRow := mocks.NewMockRow(ctrl) mockRow.EXPECT().Scan(gomock.Any()).SetArg(0, float64(100.1)).Return(nil) mockConn.EXPECT(). QueryRow(gomock.Any(), `SELECT COUNT(*)/300 AS rate FROM flows WHERE TimeReceived > date_sub(minute, 5, now())`). Return(mockRow) helpers.TestHTTPEndpoints(t, h.LocalAddr(), helpers.HTTPEndpointCases{ { URL: "/api/v0/console/widget/flow-rate", JSONOutput: gin.H{ "period": "second", "rate": 100.1, }, }, }) } func TestWidgetExporters(t *testing.T) { _, h, mockConn, _ := NewMock(t, DefaultConfiguration()) expected := []struct { ExporterName string }{ {"exporter1"}, {"exporter2"}, {"exporter3"}, } mockConn.EXPECT(). Select(gomock.Any(), gomock.Any(), `SELECT ExporterName FROM exporters GROUP BY ExporterName ORDER BY ExporterName`). SetArg(1, expected). Return(nil) helpers.TestHTTPEndpoints(t, h.LocalAddr(), helpers.HTTPEndpointCases{ { URL: "/api/v0/console/widget/exporters", JSONOutput: gin.H{ "exporters": []string{ "exporter1", "exporter2", "exporter3", }, }, }, }) } func TestWidgetTop(t *testing.T) { _, h, mockConn, _ := NewMock(t, DefaultConfiguration()) gomock.InOrder( mockConn.EXPECT(). Select(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil). SetArg(1, []topResult{ {"TCP/443", float64(51)}, {"UDP/443", float64(20)}, {"TCP/80", float64(18)}, }), mockConn.EXPECT(). Select(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil). SetArg(1, []topResult{ {"TCP", float64(75)}, {"UDP", float64(24)}, {"ESP", float64(1)}, }), mockConn.EXPECT(). Select(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil). SetArg(1, []topResult{ {"exporter1", float64(20)}, {"exporter3", float64(10)}, {"exporter5", float64(3)}, }), mockConn.EXPECT(). Select(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil). SetArg(1, []topResult{ {"2906: Netflix", float64(12)}, {"36040: Youtube", float64(10)}, {"20940: Akamai", float64(9)}, }), mockConn.EXPECT(). Select(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil). SetArg(1, []topResult{ {"TCP/443", float64(80)}, {"TCP/80", float64(18)}, }), ) helpers.TestHTTPEndpoints(t, h.LocalAddr(), helpers.HTTPEndpointCases{ { URL: "/api/v0/console/widget/top/src-port", JSONOutput: gin.H{ "top": []gin.H{ {"name": "TCP/443", "percent": 51}, {"name": "UDP/443", "percent": 20}, {"name": "TCP/80", "percent": 18}, }, }, }, { URL: "/api/v0/console/widget/top/protocol", JSONOutput: gin.H{ "top": []gin.H{ {"name": "TCP", "percent": 75}, {"name": "UDP", "percent": 24}, {"name": "ESP", "percent": 1}, }, }, }, { URL: "/api/v0/console/widget/top/exporter", JSONOutput: gin.H{ "top": []gin.H{ {"name": "exporter1", "percent": 20}, {"name": "exporter3", "percent": 10}, {"name": "exporter5", "percent": 3}, }, }, }, { URL: "/api/v0/console/widget/top/src-as", JSONOutput: gin.H{ "top": []gin.H{ {"name": "2906: Netflix", "percent": 12}, {"name": "36040: Youtube", "percent": 10}, {"name": "20940: Akamai", "percent": 9}, }, }, }, { URL: "/api/v0/console/widget/top/dst-port", JSONOutput: gin.H{ "top": []gin.H{ {"name": "TCP/443", "percent": 80}, {"name": "TCP/80", "percent": 18}, }, }, }, { URL: "/api/v0/console/widget/top/notexist", StatusCode: 400, JSONOutput: gin.H{ "message": "Notexist does not belong to HomepageTopWidget values", }, }, }) } func TestWidgetGraph(t *testing.T) { testcases := []struct { config Configuration query string }{ { config: DefaultConfiguration(), query: ` SELECT toStartOfInterval(TimeReceived + INTERVAL 144 second, INTERVAL 432 second) - INTERVAL 144 second AS Time, SUM(Bytes*SamplingRate*8/432)/1000/1000/1000 AS Gbps FROM flows WHERE TimeReceived BETWEEN toDateTime('2009-11-10 23:00:00', 'UTC') AND toDateTime('2009-11-11 23:00:00', 'UTC') AND InIfBoundary = 'external' GROUP BY Time ORDER BY Time WITH FILL FROM toDateTime('2009-11-10 23:00:00', 'UTC') TO toDateTime('2009-11-11 23:00:00', 'UTC') + INTERVAL 1 second STEP 432`, }, { config: func() Configuration { c := DefaultConfiguration() c.HomepageGraphFilter = "" return c }(), query: ` SELECT toStartOfInterval(TimeReceived + INTERVAL 144 second, INTERVAL 432 second) - INTERVAL 144 second AS Time, SUM(Bytes*SamplingRate*8/432)/1000/1000/1000 AS Gbps FROM flows WHERE TimeReceived BETWEEN toDateTime('2009-11-10 23:00:00', 'UTC') AND toDateTime('2009-11-11 23:00:00', 'UTC') GROUP BY Time ORDER BY Time WITH FILL FROM toDateTime('2009-11-10 23:00:00', 'UTC') TO toDateTime('2009-11-11 23:00:00', 'UTC') + INTERVAL 1 second STEP 432`, }, { config: func() Configuration { c := DefaultConfiguration() c.HomepageGraphFilter = "OutIfBoundary = 'external'" return c }(), query: ` SELECT toStartOfInterval(TimeReceived + INTERVAL 144 second, INTERVAL 432 second) - INTERVAL 144 second AS Time, SUM(Bytes*SamplingRate*8/432)/1000/1000/1000 AS Gbps FROM flows WHERE TimeReceived BETWEEN toDateTime('2009-11-10 23:00:00', 'UTC') AND toDateTime('2009-11-11 23:00:00', 'UTC') AND OutIfBoundary = 'external' GROUP BY Time ORDER BY Time WITH FILL FROM toDateTime('2009-11-10 23:00:00', 'UTC') TO toDateTime('2009-11-11 23:00:00', 'UTC') + INTERVAL 1 second STEP 432`, }, } for _, tcase := range testcases { _, h, mockConn, mockClock := NewMock(t, tcase.config) base := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) mockClock.Set(base.Add(24 * time.Hour)) expected := []struct { Time time.Time `json:"t"` Gbps float64 `json:"gbps"` }{ {base, 25.3}, {base.Add(time.Minute), 27.8}, {base.Add(2 * time.Minute), 26.4}, {base.Add(3 * time.Minute), 29.2}, {base.Add(4 * time.Minute), 0}, {base.Add(5 * time.Minute), 24.7}, } mockConn.EXPECT(). Select(gomock.Any(), gomock.Any(), strings.TrimSpace(tcase.query)). SetArg(1, expected). Return(nil) helpers.TestHTTPEndpoints(t, h.LocalAddr(), helpers.HTTPEndpointCases{ { URL: "/api/v0/console/widget/graph", JSONOutput: gin.H{ "data": []gin.H{ {"t": "2009-11-10T23:00:00Z", "gbps": 25.3}, {"t": "2009-11-10T23:01:00Z", "gbps": 27.8}, {"t": "2009-11-10T23:02:00Z", "gbps": 26.4}, {"t": "2009-11-10T23:03:00Z", "gbps": 29.2}, {"t": "2009-11-10T23:04:00Z", "gbps": 0}, {"t": "2009-11-10T23:05:00Z", "gbps": 24.7}, }, }, }, }) } }