// SPDX-FileCopyrightText: 2022 Free Mobile // SPDX-License-Identifier: AGPL-3.0-only package console import ( "fmt" "testing" "time" "github.com/gin-gonic/gin" "go.uber.org/mock/gomock" "akvorado/common/helpers" "akvorado/common/schema" "akvorado/console/query" ) func TestGraphLineInputReverseDirection(t *testing.T) { input := graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ schema: schema.NewMock(t), Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Dimensions: query.Columns{ query.NewColumn("ExporterName"), query.NewColumn("InIfProvider"), }, Filter: query.NewFilter("DstCountry = 'FR' AND SrcCountry = 'US'"), Units: "l3bps", }, Points: 100, } original1 := fmt.Sprintf("%+v", input) expected := graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Dimensions: query.Columns{ query.NewColumn("ExporterName"), query.NewColumn("OutIfProvider"), }, Filter: query.NewFilter("SrcCountry = 'FR' AND DstCountry = 'US'"), Units: "l3bps", }, Points: 100, } input.Filter.Validate(input.schema) expected.Filter.Validate(input.schema) query.Columns(input.Dimensions).Validate(input.schema) query.Columns(expected.Dimensions).Validate(input.schema) got := input.reverseDirection() original2 := fmt.Sprintf("%+v", input) if diff := helpers.Diff(got, expected); diff != "" { t.Fatalf("reverseDirection() (-got, +want):\n%s", diff) } if original1 != original2 { t.Fatalf("reverseDirection() modified original to:\n-%s\n+%s", original1, original2) } } func TestGraphPreviousPeriod(t *testing.T) { const longForm = "Jan 2, 2006 at 15:04" cases := []struct { Pos helpers.Pos Start string End string ExpectedStart string ExpectedEnd string }{ { helpers.Mark(), "Jan 2, 2020 at 15:04", "Jan 2, 2020 at 16:04", "Jan 2, 2020 at 14:04", "Jan 2, 2020 at 15:04", }, { helpers.Mark(), "Jan 2, 2020 at 15:04", "Jan 2, 2020 at 16:34", "Jan 2, 2020 at 14:04", "Jan 2, 2020 at 15:34", }, { helpers.Mark(), "Jan 2, 2020 at 15:04", "Jan 2, 2020 at 17:34", "Jan 1, 2020 at 15:04", "Jan 1, 2020 at 17:34", }, { helpers.Mark(), "Jan 2, 2020 at 15:04", "Jan 3, 2020 at 17:34", "Jan 1, 2020 at 15:04", "Jan 2, 2020 at 17:34", }, { helpers.Mark(), "Jan 10, 2020 at 15:04", "Jan 13, 2020 at 17:34", "Jan 3, 2020 at 15:04", "Jan 6, 2020 at 17:34", }, { helpers.Mark(), "Jan 10, 2020 at 15:04", "Jan 15, 2020 at 17:34", "Jan 3, 2020 at 15:04", "Jan 8, 2020 at 17:34", }, { helpers.Mark(), "Jan 10, 2020 at 15:04", "Jan 20, 2020 at 17:34", "Jan 3, 2020 at 15:04", "Jan 13, 2020 at 17:34", }, { helpers.Mark(), "Feb 10, 2020 at 15:04", "Feb 25, 2020 at 17:34", "Jan 13, 2020 at 15:04", "Jan 28, 2020 at 17:34", }, { helpers.Mark(), "Feb 10, 2020 at 15:04", "Mar 25, 2020 at 17:34", "Jan 13, 2020 at 15:04", "Feb 26, 2020 at 17:34", }, { helpers.Mark(), "Feb 10, 2020 at 15:04", "Jul 25, 2020 at 17:34", "Feb 10, 2019 at 15:04", "Jul 25, 2019 at 17:34", }, { helpers.Mark(), "Feb 10, 2019 at 15:04", "Jul 25, 2020 at 17:34", "Feb 10, 2018 at 15:04", "Jul 25, 2019 at 17:34", }, } for _, tc := range cases { t.Run(fmt.Sprintf("%s to %s", tc.Start, tc.End), func(t *testing.T) { start, err := time.Parse(longForm, tc.Start) if err != nil { t.Fatalf("%stime.Parse(%q) error:\n%+v", tc.Pos, tc.Start, err) } end, err := time.Parse(longForm, tc.End) if err != nil { t.Fatalf("%stime.Parse(%q) error:\n%+v", tc.Pos, tc.End, err) } expectedStart, err := time.Parse(longForm, tc.ExpectedStart) if err != nil { t.Fatalf("%stime.Parse(%q) error:\n%+v", tc.Pos, tc.ExpectedStart, err) } expectedEnd, err := time.Parse(longForm, tc.ExpectedEnd) if err != nil { t.Fatalf("%stime.Parse(%q) error:\n%+v", tc.Pos, tc.ExpectedEnd, err) } input := graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ schema: schema.NewMock(t), Start: start, End: end, Dimensions: query.Columns{ query.NewColumn("ExporterAddress"), query.NewColumn("ExporterName"), }, }, } query.Columns(input.Dimensions).Validate(input.schema) got := input.previousPeriod() expected := graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ Start: expectedStart, End: expectedEnd, Dimensions: []query.Column{}, }, } if diff := helpers.Diff(got, expected); diff != "" { t.Fatalf("%spreviousPeriod() (-got, +want):\n%s", tc.Pos, diff) } }) } } func TestGraphQuerySQL(t *testing.T) { cases := []struct { Description string Pos helpers.Pos Input graphLineHandlerInput Expected []templateQuery }{ { Description: "no dimensions, no filters, bps", Pos: helpers.Mark(), Input: graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Dimensions: []query.Column{}, Filter: query.Filter{}, Units: "l3bps", }, Points: 100, }, Expected: []templateQuery{ { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Points: 100, Units: "l3bps", }, Template: `WITH source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1) SELECT 1 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, emptyArrayString() AS dimensions FROM source WHERE {{ .Timefilter }} GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS emptyArrayString()))`, }, }, }, { Description: "no dimensions, no filters, l2 bps", Pos: helpers.Mark(), Input: graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Dimensions: []query.Column{}, Filter: query.Filter{}, Units: "l2bps", }, Points: 100, }, Expected: []templateQuery{ { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Points: 100, Units: "l2bps", }, Template: `WITH source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1) SELECT 1 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, emptyArrayString() AS dimensions FROM source WHERE {{ .Timefilter }} GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS emptyArrayString()))`, }, }, }, { Description: "no dimensions, no filters, pps", Pos: helpers.Mark(), Input: graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Dimensions: []query.Column{}, Filter: query.Filter{}, Units: "pps", }, Points: 100, }, Expected: []templateQuery{ { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Points: 100, Units: "pps", }, Template: `WITH source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1) SELECT 1 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, emptyArrayString() AS dimensions FROM source WHERE {{ .Timefilter }} GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS emptyArrayString()))`, }, }, }, { Description: "truncated source address", Pos: helpers.Mark(), Input: graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Dimensions: []query.Column{query.NewColumn("SrcAddr")}, Filter: query.NewFilter("SrcAddr << 1.0.0.0/8"), TruncateAddrV4: 24, TruncateAddrV6: 48, Units: "l3bps", }, Points: 100, }, Expected: []templateQuery{ { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Points: 100, Units: "l3bps", MainTableRequired: true, }, Template: `WITH source AS (SELECT * REPLACE (tupleElement(IPv6CIDRToRange(SrcAddr, if(tupleElement(IPv6CIDRToRange(SrcAddr, 96), 1) = toIPv6('::ffff:0.0.0.0'), 120, 48)), 1) AS SrcAddr) FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1), rows AS (SELECT SrcAddr FROM source WHERE {{ .Timefilter }} AND (SrcAddr BETWEEN toIPv6('::ffff:1.0.0.0') AND toIPv6('::ffff:1.255.255.255')) GROUP BY SrcAddr ORDER BY {{ .Units }} DESC LIMIT 0) SELECT 1 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, if((SrcAddr) IN rows, [replaceRegexpOne(IPv6NumToString(SrcAddr), '^::ffff:', '')], ['Other']) AS dimensions FROM source WHERE {{ .Timefilter }} AND (SrcAddr BETWEEN toIPv6('::ffff:1.0.0.0') AND toIPv6('::ffff:1.255.255.255')) GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS ['Other']))`, }, }, }, { Description: "no dimensions", Pos: helpers.Mark(), Input: graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Dimensions: []query.Column{}, Filter: query.NewFilter("DstCountry = 'FR' AND SrcCountry = 'US'"), Units: "l3bps", }, Points: 100, }, Expected: []templateQuery{ { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Points: 100, Units: "l3bps", }, Template: `WITH source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1) SELECT 1 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, emptyArrayString() AS dimensions FROM source WHERE {{ .Timefilter }} AND (DstCountry = 'FR' AND SrcCountry = 'US') GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS emptyArrayString()))`, }, }, }, { Description: "no dimensions, escaped filter", Pos: helpers.Mark(), Input: graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Dimensions: []query.Column{}, Filter: query.NewFilter("InIfDescription = '{{ hello }}' AND SrcCountry = 'US'"), Units: "l3bps", }, Points: 100, }, Expected: []templateQuery{ { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Points: 100, Units: "l3bps", }, Template: `WITH source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1) SELECT 1 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, emptyArrayString() AS dimensions FROM source WHERE {{ .Timefilter }} AND (InIfDescription = '{{"{{"}} hello }}' AND SrcCountry = 'US') GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS emptyArrayString()))`, }, }, }, { Description: "no dimensions, reverse direction", Pos: helpers.Mark(), Input: graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Dimensions: []query.Column{}, Filter: query.NewFilter("DstCountry = 'FR' AND SrcCountry = 'US'"), Units: "l3bps", }, Points: 100, Bidirectional: true, }, Expected: []templateQuery{ { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Points: 100, Units: "l3bps", }, Template: `WITH source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1) SELECT 1 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, emptyArrayString() AS dimensions FROM source WHERE {{ .Timefilter }} AND (DstCountry = 'FR' AND SrcCountry = 'US') GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS emptyArrayString()))`, }, { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Points: 100, Units: "l3bps", }, Template: `SELECT 2 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, emptyArrayString() AS dimensions FROM source WHERE {{ .Timefilter }} AND (SrcCountry = 'FR' AND DstCountry = 'US') GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS emptyArrayString()))`, }, }, }, { Description: "no dimensions, reverse direction, inl2%", Pos: helpers.Mark(), Input: graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Dimensions: []query.Column{}, Filter: query.NewFilter("DstCountry = 'FR' AND SrcCountry = 'US'"), Units: "inl2%", }, Points: 100, Bidirectional: true, }, Expected: []templateQuery{ { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Points: 100, Units: "inl2%", }, Template: `WITH source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1) SELECT 1 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, emptyArrayString() AS dimensions FROM source WHERE {{ .Timefilter }} AND (DstCountry = 'FR' AND SrcCountry = 'US') GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS emptyArrayString()))`, }, { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Points: 100, Units: "outl2%", }, Template: `SELECT 2 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, emptyArrayString() AS dimensions FROM source WHERE {{ .Timefilter }} AND (SrcCountry = 'FR' AND DstCountry = 'US') GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS emptyArrayString()))`, }, }, }, { Description: "no filters", Pos: helpers.Mark(), Input: graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Limit: 20, Dimensions: []query.Column{ query.NewColumn("ExporterName"), query.NewColumn("InIfProvider"), }, Filter: query.Filter{}, Units: "l3bps", }, Points: 100, }, Expected: []templateQuery{ { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Points: 100, Units: "l3bps", }, Template: `WITH source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1), rows AS (SELECT ExporterName, InIfProvider FROM source WHERE {{ .Timefilter }} GROUP BY ExporterName, InIfProvider ORDER BY {{ .Units }} DESC LIMIT 20) SELECT 1 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, if((ExporterName, InIfProvider) IN rows, [ExporterName, InIfProvider], ['Other', 'Other']) AS dimensions FROM source WHERE {{ .Timefilter }} GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS ['Other', 'Other']))`, }, }, }, { Description: "no filters, limitType by max", Pos: helpers.Mark(), Input: graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Limit: 20, LimitType: "max", Dimensions: []query.Column{ query.NewColumn("ExporterName"), query.NewColumn("InIfProvider"), }, Filter: query.Filter{}, Units: "l3bps", }, Points: 100, }, Expected: []templateQuery{ { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Points: 100, Units: "l3bps", }, Template: `WITH source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1), rows AS (SELECT ExporterName, InIfProvider FROM ( SELECT ExporterName, InIfProvider, {{ .Units }} AS sum_at_time FROM source WHERE {{ .Timefilter }} GROUP BY ExporterName, InIfProvider ) GROUP BY ExporterName, InIfProvider ORDER BY MAX(sum_at_time) DESC LIMIT 20) SELECT 1 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, if((ExporterName, InIfProvider) IN rows, [ExporterName, InIfProvider], ['Other', 'Other']) AS dimensions FROM source WHERE {{ .Timefilter }} GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS ['Other', 'Other']))`, }, }, }, { Description: "no filters, reverse", Pos: helpers.Mark(), Input: graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Limit: 20, Dimensions: []query.Column{ query.NewColumn("ExporterName"), query.NewColumn("InIfProvider"), }, Filter: query.Filter{}, Units: "l3bps", }, Points: 100, Bidirectional: true, }, Expected: []templateQuery{ { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Points: 100, Units: "l3bps", }, Template: `WITH source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1), rows AS (SELECT ExporterName, InIfProvider FROM source WHERE {{ .Timefilter }} GROUP BY ExporterName, InIfProvider ORDER BY {{ .Units }} DESC LIMIT 20) SELECT 1 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, if((ExporterName, InIfProvider) IN rows, [ExporterName, InIfProvider], ['Other', 'Other']) AS dimensions FROM source WHERE {{ .Timefilter }} GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS ['Other', 'Other']))`, }, { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Points: 100, Units: "l3bps", }, Template: `SELECT 2 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, if((ExporterName, OutIfProvider) IN rows, [ExporterName, OutIfProvider], ['Other', 'Other']) AS dimensions FROM source WHERE {{ .Timefilter }} GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS ['Other', 'Other']))`, }, }, }, { Description: "no filters, previous period", Pos: helpers.Mark(), Input: graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Limit: 20, Dimensions: []query.Column{ query.NewColumn("ExporterName"), query.NewColumn("InIfProvider"), }, Filter: query.Filter{}, Units: "l3bps", }, Points: 100, PreviousPeriod: true, }, Expected: []templateQuery{ { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Points: 100, Units: "l3bps", }, Template: `WITH source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1), rows AS (SELECT ExporterName, InIfProvider FROM source WHERE {{ .Timefilter }} GROUP BY ExporterName, InIfProvider ORDER BY {{ .Units }} DESC LIMIT 20) SELECT 1 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, if((ExporterName, InIfProvider) IN rows, [ExporterName, InIfProvider], ['Other', 'Other']) AS dimensions FROM source WHERE {{ .Timefilter }} GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS ['Other', 'Other']))`, }, { Context: inputContext{ Start: time.Date(2022, 4, 9, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), StartForTableSelection: func() *time.Time { t := time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC) return &t }(), Points: 100, Units: "l3bps", }, Template: `SELECT 3 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} + INTERVAL 86400 second AS time, {{ .Units }}/{{ .Interval }} AS xps, emptyArrayString() AS dimensions FROM source WHERE {{ .Timefilter }} GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} + INTERVAL 86400 second TO {{ .TimefilterEnd }} + INTERVAL 1 second + INTERVAL 86400 second STEP {{ .Interval }} INTERPOLATE (dimensions AS emptyArrayString()))`, }, }, }, { Description: "previous period while main table is required", Pos: helpers.Mark(), Input: graphLineHandlerInput{ graphCommonHandlerInput: graphCommonHandlerInput{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), Dimensions: []query.Column{ query.NewColumn("SrcAddr"), query.NewColumn("DstAddr"), }, Filter: query.NewFilter("InIfBoundary = external"), Units: "l3bps", }, Points: 100, PreviousPeriod: true, }, Expected: []templateQuery{ { Context: inputContext{ Start: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), MainTableRequired: true, Points: 100, Units: "l3bps", }, Template: `WITH source AS (SELECT * FROM {{ .Table }} SETTINGS asterisk_include_alias_columns = 1), rows AS (SELECT SrcAddr, DstAddr FROM source WHERE {{ .Timefilter }} AND (InIfBoundary = 'external') GROUP BY SrcAddr, DstAddr ORDER BY {{ .Units }} DESC LIMIT 0) SELECT 1 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} AS time, {{ .Units }}/{{ .Interval }} AS xps, if((SrcAddr, DstAddr) IN rows, [replaceRegexpOne(IPv6NumToString(SrcAddr), '^::ffff:', ''), replaceRegexpOne(IPv6NumToString(DstAddr), '^::ffff:', '')], ['Other', 'Other']) AS dimensions FROM source WHERE {{ .Timefilter }} AND (InIfBoundary = 'external') GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} TO {{ .TimefilterEnd }} + INTERVAL 1 second STEP {{ .Interval }} INTERPOLATE (dimensions AS ['Other', 'Other']))`, }, { Context: inputContext{ Start: time.Date(2022, 4, 9, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), StartForTableSelection: func() *time.Time { t := time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC) return &t }(), MainTableRequired: true, Points: 100, Units: "l3bps", }, Template: `SELECT 3 AS axis, * FROM ( SELECT {{ call .ToStartOfInterval "TimeReceived" }} + INTERVAL 86400 second AS time, {{ .Units }}/{{ .Interval }} AS xps, emptyArrayString() AS dimensions FROM source WHERE {{ .Timefilter }} AND (InIfBoundary = 'external') GROUP BY time, dimensions ORDER BY time WITH FILL FROM {{ .TimefilterStart }} + INTERVAL 86400 second TO {{ .TimefilterEnd }} + INTERVAL 1 second + INTERVAL 86400 second STEP {{ .Interval }} INTERPOLATE (dimensions AS emptyArrayString()))`, }, }, }, } for _, tc := range cases { tc.Input.schema = schema.NewMock(t) if err := query.Columns(tc.Input.Dimensions).Validate(tc.Input.schema); err != nil { t.Fatalf("%sValidate() error:\n%+v", tc.Pos, err) } if err := tc.Input.Filter.Validate(tc.Input.schema); err != nil { t.Fatalf("%sValidate() error:\n%+v", tc.Pos, err) } t.Run(tc.Description, func(t *testing.T) { got := tc.Input.toSQL() if diff := helpers.Diff(got, tc.Expected); diff != "" { t.Errorf("%stoSQL (-got, +want):\n%s", tc.Pos, diff) } }) } } func TestGraphLineHandler(t *testing.T) { _, h, mockConn, _ := NewMock(t, DefaultConfiguration()) base := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) t.Run("sort by avg", func(t *testing.T) { // Single direction expectedSQL := []struct { Axis uint8 `ch:"axis"` Time time.Time `ch:"time"` Xps float64 `ch:"xps"` Dimensions []string `ch:"dimensions"` }{ {1, base, 1000, []string{"router1", "provider1"}}, {1, base, 2000, []string{"router1", "provider2"}}, {1, base, 1200, []string{"router2", "provider2"}}, {1, base, 1100, []string{"router2", "provider3"}}, {1, base, 1900, []string{"Other", "Other"}}, {1, base.Add(time.Minute), 500, []string{"router1", "provider1"}}, {1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}}, {1, base.Add(time.Minute), 900, []string{"router2", "provider4"}}, {1, base.Add(time.Minute), 100, []string{"Other", "Other"}}, {1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}}, {1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}}, {1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}}, {1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}}, } mockConn.EXPECT(). Select(gomock.Any(), gomock.Any(), gomock.Any()). SetArg(1, expectedSQL). Return(nil) // Bidirectional expectedSQL = []struct { Axis uint8 `ch:"axis"` Time time.Time `ch:"time"` Xps float64 `ch:"xps"` Dimensions []string `ch:"dimensions"` }{ {1, base, 1000, []string{"router1", "provider1"}}, {1, base, 2000, []string{"router1", "provider2"}}, {1, base, 1200, []string{"router2", "provider2"}}, {1, base, 1100, []string{"router2", "provider3"}}, {1, base, 1900, []string{"Other", "Other"}}, {1, base.Add(time.Minute), 500, []string{"router1", "provider1"}}, {1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}}, {1, base.Add(time.Minute), 900, []string{"router2", "provider4"}}, // Axes can be mixed. In reality, it seems they cannot // be interleaved, but ClickHouse documentation does // not say it is not possible. {2, base, 101, []string{"router1", "provider1"}}, {2, base, 200, []string{"router1", "provider2"}}, {2, base, 120, []string{"router2", "provider2"}}, {1, base.Add(time.Minute), 100, []string{"Other", "Other"}}, {1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}}, {2, base, 111, []string{"router2", "provider3"}}, {2, base, 190, []string{"Other", "Other"}}, {2, base.Add(time.Minute), 50, []string{"router1", "provider1"}}, {2, base.Add(time.Minute), 500, []string{"router1", "provider2"}}, {1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}}, {1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}}, {1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}}, {2, base.Add(time.Minute), 90, []string{"router2", "provider4"}}, {2, base.Add(time.Minute), 10, []string{"Other", "Other"}}, {2, base.Add(2 * time.Minute), 10, []string{"router1", "provider1"}}, {2, base.Add(2 * time.Minute), 300, []string{"router1", "provider2"}}, {2, base.Add(2 * time.Minute), 10, []string{"router2", "provider4"}}, {2, base.Add(2 * time.Minute), 10, []string{"Other", "Other"}}, } mockConn.EXPECT(). Select(gomock.Any(), gomock.Any(), gomock.Any()). SetArg(1, expectedSQL). Return(nil) // Previous period expectedSQL = []struct { Axis uint8 `ch:"axis"` Time time.Time `ch:"time"` Xps float64 `ch:"xps"` Dimensions []string `ch:"dimensions"` }{ {1, base, 1000, []string{"router1", "provider1"}}, {1, base, 2000, []string{"router1", "provider2"}}, {1, base, 1200, []string{"router2", "provider2"}}, {1, base, 1100, []string{"router2", "provider3"}}, {1, base, 1900, []string{"Other", "Other"}}, {1, base.Add(time.Minute), 500, []string{"router1", "provider1"}}, {1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}}, {1, base.Add(time.Minute), 900, []string{"router2", "provider4"}}, {1, base.Add(time.Minute), 100, []string{"Other", "Other"}}, {1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}}, {1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}}, {1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}}, {1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}}, {3, base, 8000, []string{}}, {3, base.Add(time.Minute), 6000, []string{}}, {3, base.Add(2 * time.Minute), 4500, []string{}}, } mockConn.EXPECT(). Select(gomock.Any(), gomock.Any(), gomock.Any()). SetArg(1, expectedSQL). Return(nil) helpers.TestHTTPEndpoints(t, h.LocalAddr(), helpers.HTTPEndpointCases{ { Description: "single direction", URL: "/api/v0/console/graph/line", JSONInput: gin.H{ "start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), "end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), "points": 100, "limit": 20, "limitType": "avg", "dimensions": []string{"ExporterName", "InIfProvider"}, "filter": "DstCountry = 'FR' AND SrcCountry = 'US'", "units": "l3bps", "bidirectional": false, }, JSONOutput: gin.H{ // Sorted by sum of bps "rows": [][]string{ {"router1", "provider2"}, // 10000 {"router1", "provider1"}, // 1600 {"router2", "provider2"}, // 1200 {"router2", "provider3"}, // 1100 {"router2", "provider4"}, // 1000 {"Other", "Other"}, // 2100 }, "t": []string{ "2009-11-10T23:00:00Z", "2009-11-10T23:01:00Z", "2009-11-10T23:02:00Z", }, "points": [][]int{ {2000, 5000, 3000}, {1000, 500, 100}, {1200, 0, 0}, {1100, 0, 0}, {0, 900, 100}, {1900, 100, 100}, }, "min": []int{ 2000, 100, 1200, 1100, 100, 100, }, "max": []int{ 5000, 1000, 1200, 1100, 900, 1900, }, "last": []int{ 5000, 500, 0, 0, 900, 100, }, "average": []int{ 3333, 533, 400, 366, 333, 700, }, "95th": []int{ 4800, 950, 1080, 990, 820, 1720, }, "axis": []int{ 1, 1, 1, 1, 1, 1, }, "axis-names": map[int]string{ 1: "Direct", }, }, }, { Description: "bidirectional", URL: "/api/v0/console/graph/line", JSONInput: gin.H{ "start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), "end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), "points": 100, "limit": 20, "limitType": "avg", "dimensions": []string{"ExporterName", "InIfProvider"}, "filter": "DstCountry = 'FR' AND SrcCountry = 'US'", "units": "l3bps", "bidirectional": true, }, JSONOutput: gin.H{ // Sorted by sum of bps "rows": [][]string{ {"router1", "provider2"}, // 10000 {"router1", "provider1"}, // 1600 {"router2", "provider2"}, // 1200 {"router2", "provider3"}, // 1100 {"router2", "provider4"}, // 1000 {"Other", "Other"}, // 2100 {"router1", "provider2"}, // 1000 {"router1", "provider1"}, // 160 {"router2", "provider2"}, // 120 {"router2", "provider3"}, // 110 {"router2", "provider4"}, // 100 {"Other", "Other"}, // 210 }, "t": []string{ "2009-11-10T23:00:00Z", "2009-11-10T23:01:00Z", "2009-11-10T23:02:00Z", }, "points": [][]int{ {2000, 5000, 3000}, {1000, 500, 100}, {1200, 0, 0}, {1100, 0, 0}, {0, 900, 100}, {1900, 100, 100}, {200, 500, 300}, {101, 50, 10}, {120, 0, 0}, {111, 0, 0}, {0, 90, 10}, {190, 10, 10}, }, "min": []int{ 2000, 100, 1200, 1100, 100, 100, 200, 10, 120, 111, 10, 10, }, "max": []int{ 5000, 1000, 1200, 1100, 900, 1900, 500, 101, 120, 111, 90, 190, }, "last": []int{ 5000, 500, 0, 0, 900, 100, 500, 50, 0, 0, 90, 10, }, "average": []int{ 3333, 533, 400, 366, 333, 700, 333, 53, 40, 37, 33, 70, }, "95th": []int{ 4800, 950, 1080, 990, 820, 1720, 480, 96, 108, 100, 82, 172, }, "axis": []int{ 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, }, "axis-names": map[int]string{ 1: "Direct", 2: "Reverse", }, }, }, { Description: "previous period", URL: "/api/v0/console/graph/line", JSONInput: gin.H{ "start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), "end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), "points": 100, "limit": 20, "limitType": "avg", "dimensions": []string{"ExporterName", "InIfProvider"}, "filter": "DstCountry = 'FR' AND SrcCountry = 'US'", "units": "l3bps", "bidirectional": false, "previous-period": true, }, JSONOutput: gin.H{ // Sorted by sum of bps "rows": [][]string{ {"router1", "provider2"}, // 10000 {"router1", "provider1"}, // 1600 {"router2", "provider2"}, // 1200 {"router2", "provider3"}, // 1100 {"router2", "provider4"}, // 1000 {"Other", "Other"}, // 2100 {"Other", "Other"}, // Previous day }, "t": []string{ "2009-11-10T23:00:00Z", "2009-11-10T23:01:00Z", "2009-11-10T23:02:00Z", }, "points": [][]int{ {2000, 5000, 3000}, {1000, 500, 100}, {1200, 0, 0}, {1100, 0, 0}, {0, 900, 100}, {1900, 100, 100}, {8000, 6000, 4500}, }, "min": []int{ 2000, 100, 1200, 1100, 100, 100, 4500, }, "max": []int{ 5000, 1000, 1200, 1100, 900, 1900, 8000, }, "last": []int{ 5000, 500, 0, 0, 900, 100, 6000, }, "average": []int{ 3333, 533, 400, 366, 333, 700, 6166, }, "95th": []int{ 4800, 950, 1080, 990, 820, 1720, 7800, }, "axis": []int{ 1, 1, 1, 1, 1, 1, 3, }, "axis-names": map[int]string{ 1: "Direct", 3: "Previous day", }, }, }, }) }) t.Run("sort by max", func(t *testing.T) { // Single direction expectedSQL := []struct { Axis uint8 `ch:"axis"` Time time.Time `ch:"time"` Xps float64 `ch:"xps"` Dimensions []string `ch:"dimensions"` }{ {1, base, 1000, []string{"router1", "provider1"}}, {1, base, 2000, []string{"router1", "provider2"}}, {1, base, 1200, []string{"router2", "provider2"}}, {1, base, 1100, []string{"router2", "provider3"}}, {1, base, 1900, []string{"Other", "Other"}}, {1, base.Add(time.Minute), 500, []string{"router1", "provider1"}}, {1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}}, {1, base.Add(time.Minute), 900, []string{"router2", "provider4"}}, {1, base.Add(time.Minute), 100, []string{"Other", "Other"}}, {1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}}, {1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}}, {1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}}, {1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}}, } mockConn.EXPECT(). Select(gomock.Any(), gomock.Any(), gomock.Any()). SetArg(1, expectedSQL). Return(nil) // Bidirectional expectedSQL = []struct { Axis uint8 `ch:"axis"` Time time.Time `ch:"time"` Xps float64 `ch:"xps"` Dimensions []string `ch:"dimensions"` }{ {1, base, 1000, []string{"router1", "provider1"}}, {1, base, 2000, []string{"router1", "provider2"}}, {1, base, 1200, []string{"router2", "provider2"}}, {1, base, 1100, []string{"router2", "provider3"}}, {1, base, 1900, []string{"Other", "Other"}}, {1, base.Add(time.Minute), 500, []string{"router1", "provider1"}}, {1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}}, {1, base.Add(time.Minute), 900, []string{"router2", "provider4"}}, // Axes can be mixed. In reality, it seems they cannot // be interleaved, but ClickHouse documentation does // not say it is not possible. {2, base, 101, []string{"router1", "provider1"}}, {2, base, 200, []string{"router1", "provider2"}}, {2, base, 120, []string{"router2", "provider2"}}, {1, base.Add(time.Minute), 100, []string{"Other", "Other"}}, {1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}}, {2, base, 111, []string{"router2", "provider3"}}, {2, base, 190, []string{"Other", "Other"}}, {2, base.Add(time.Minute), 50, []string{"router1", "provider1"}}, {2, base.Add(time.Minute), 500, []string{"router1", "provider2"}}, {1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}}, {1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}}, {1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}}, {2, base.Add(time.Minute), 90, []string{"router2", "provider4"}}, {2, base.Add(time.Minute), 10, []string{"Other", "Other"}}, {2, base.Add(2 * time.Minute), 10, []string{"router1", "provider1"}}, {2, base.Add(2 * time.Minute), 300, []string{"router1", "provider2"}}, {2, base.Add(2 * time.Minute), 10, []string{"router2", "provider4"}}, {2, base.Add(2 * time.Minute), 10, []string{"Other", "Other"}}, } mockConn.EXPECT(). Select(gomock.Any(), gomock.Any(), gomock.Any()). SetArg(1, expectedSQL). Return(nil) // Previous period expectedSQL = []struct { Axis uint8 `ch:"axis"` Time time.Time `ch:"time"` Xps float64 `ch:"xps"` Dimensions []string `ch:"dimensions"` }{ {1, base, 1000, []string{"router1", "provider1"}}, {1, base, 2000, []string{"router1", "provider2"}}, {1, base, 1200, []string{"router2", "provider2"}}, {1, base, 1100, []string{"router2", "provider3"}}, {1, base, 1900, []string{"Other", "Other"}}, {1, base.Add(time.Minute), 500, []string{"router1", "provider1"}}, {1, base.Add(time.Minute), 5000, []string{"router1", "provider2"}}, {1, base.Add(time.Minute), 900, []string{"router2", "provider4"}}, {1, base.Add(time.Minute), 100, []string{"Other", "Other"}}, {1, base.Add(2 * time.Minute), 100, []string{"router1", "provider1"}}, {1, base.Add(2 * time.Minute), 3000, []string{"router1", "provider2"}}, {1, base.Add(2 * time.Minute), 100, []string{"router2", "provider4"}}, {1, base.Add(2 * time.Minute), 100, []string{"Other", "Other"}}, {3, base, 8000, []string{}}, {3, base.Add(time.Minute), 6000, []string{}}, {3, base.Add(2 * time.Minute), 4500, []string{}}, } mockConn.EXPECT(). Select(gomock.Any(), gomock.Any(), gomock.Any()). SetArg(1, expectedSQL). Return(nil) helpers.TestHTTPEndpoints(t, h.LocalAddr(), helpers.HTTPEndpointCases{ { Description: "single direction", URL: "/api/v0/console/graph/line", JSONInput: gin.H{ "start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), "end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), "points": 100, "limit": 20, "limitType": "max", "dimensions": []string{"ExporterName", "InIfProvider"}, "filter": "DstCountry = 'FR' AND SrcCountry = 'US'", "units": "l3bps", "bidirectional": false, }, JSONOutput: gin.H{ // Sorted by max of bps "rows": [][]string{ {"router1", "provider2"}, {"router2", "provider2"}, {"router2", "provider3"}, {"router1", "provider1"}, {"router2", "provider4"}, {"Other", "Other"}, }, "t": []string{ "2009-11-10T23:00:00Z", "2009-11-10T23:01:00Z", "2009-11-10T23:02:00Z", }, "points": [][]int{ {2000, 5000, 3000}, {1200, 0, 0}, {1100, 0, 0}, {1000, 500, 100}, {0, 900, 100}, {1900, 100, 100}, }, "min": []int{ 2000, 1200, 1100, 100, 100, 100, }, "max": []int{ 5000, 1200, 1100, 1000, 900, 1900, }, "last": []int{ 5000, 0, 0, 500, 900, 100, }, "average": []int{ 3333, 400, 366, 533, 333, 700, }, "95th": []int{ 4800, 1080, 990, 950, 820, 1720, }, "axis": []int{ 1, 1, 1, 1, 1, 1, }, "axis-names": map[int]string{ 1: "Direct", }, }, }, { Description: "bidirectional", URL: "/api/v0/console/graph/line", JSONInput: gin.H{ "start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), "end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), "points": 100, "limit": 20, "limitType": "max", "dimensions": []string{"ExporterName", "InIfProvider"}, "filter": "DstCountry = 'FR' AND SrcCountry = 'US'", "units": "l3bps", "bidirectional": true, }, JSONOutput: gin.H{ // Sorted by sum of bps "rows": [][]string{ {"router1", "provider2"}, // 10000 {"router2", "provider2"}, // 1200 {"router2", "provider3"}, // 1100 {"router1", "provider1"}, // 1600 {"router2", "provider4"}, // 1000 {"Other", "Other"}, // 2100 {"router1", "provider2"}, // 1000 {"router2", "provider2"}, // 120 {"router2", "provider3"}, // 110 {"router1", "provider1"}, // 160 {"router2", "provider4"}, // 100 {"Other", "Other"}, // 210 }, "t": []string{ "2009-11-10T23:00:00Z", "2009-11-10T23:01:00Z", "2009-11-10T23:02:00Z", }, "points": [][]int{ {2000, 5000, 3000}, {1200, 0, 0}, {1100, 0, 0}, {1000, 500, 100}, {0, 900, 100}, {1900, 100, 100}, {200, 500, 300}, {120, 0, 0}, {111, 0, 0}, {101, 50, 10}, {0, 90, 10}, {190, 10, 10}, }, "min": []int{ 2000, 1200, 1100, 100, 100, 100, 200, 120, 111, 10, 10, 10, }, "max": []int{ 5000, 1200, 1100, 1000, 900, 1900, 500, 120, 111, 101, 90, 190, }, "last": []int{ 5000, 0, 0, 500, 900, 100, 500, 0, 0, 50, 90, 10, }, "average": []int{ 3333, 400, 366, 533, 333, 700, 333, 40, 37, 53, 33, 70, }, "95th": []int{ 4800, 1080, 990, 950, 820, 1720, 480, 108, 100, 96, 82, 172, }, "axis": []int{ 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, }, "axis-names": map[int]string{ 1: "Direct", 2: "Reverse", }, }, }, { Description: "previous period", URL: "/api/v0/console/graph/line", JSONInput: gin.H{ "start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), "end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), "points": 100, "limit": 20, "limitType": "max", "dimensions": []string{"ExporterName", "InIfProvider"}, "filter": "DstCountry = 'FR' AND SrcCountry = 'US'", "units": "l3bps", "bidirectional": false, "previous-period": true, }, JSONOutput: gin.H{ // Sorted by sum of bps "rows": [][]string{ {"router1", "provider2"}, // 10000 {"router2", "provider2"}, // 1200 {"router2", "provider3"}, // 1100 {"router1", "provider1"}, // 1600 {"router2", "provider4"}, // 1000 {"Other", "Other"}, // 2100 {"Other", "Other"}, // Previous day }, "t": []string{ "2009-11-10T23:00:00Z", "2009-11-10T23:01:00Z", "2009-11-10T23:02:00Z", }, "points": [][]int{ {2000, 5000, 3000}, {1200, 0, 0}, {1100, 0, 0}, {1000, 500, 100}, {0, 900, 100}, {1900, 100, 100}, {8000, 6000, 4500}, }, "min": []int{ 2000, 1200, 1100, 100, 100, 100, 4500, }, "max": []int{ 5000, 1200, 1100, 1000, 900, 1900, 8000, }, "last": []int{ 5000, 0, 0, 500, 900, 100, 6000, }, "average": []int{ 3333, 400, 366, 533, 333, 700, 6166, }, "95th": []int{ 4800, 1080, 990, 950, 820, 1720, 7800, }, "axis": []int{ 1, 1, 1, 1, 1, 1, 3, }, "axis-names": map[int]string{ 1: "Direct", 3: "Previous day", }, }, }, }) }) } func TestGetTableInterval(t *testing.T) { _, h, _, mockClock := NewMock(t, DefaultConfiguration()) mockClock.Set(time.Date(2022, 4, 12, 15, 45, 10, 0, time.UTC)) helpers.TestHTTPEndpoints(t, h.LocalAddr(), helpers.HTTPEndpointCases{ { Description: "simple query", URL: "/api/v0/console/graph/table-interval", JSONInput: gin.H{ "start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), "end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), "points": 300, }, JSONOutput: gin.H{ "table": "flows", "interval": 1, }, }, { Description: "too many points", URL: "/api/v0/console/graph/table-interval", JSONInput: gin.H{ "start": time.Date(2022, 4, 10, 15, 45, 10, 0, time.UTC), "end": time.Date(2022, 4, 11, 15, 45, 10, 0, time.UTC), "points": 86400, }, StatusCode: 400, JSONOutput: gin.H{ "message": `Key: 'tableIntervalInput.Points' Error:Field validation for 'Points' failed on the 'max' tag`, }, }, }) }