package neptune_apex import ( "context" "net" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/require" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/testutil" ) func TestGather(t *testing.T) { h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { if _, err := w.Write([]byte("data")); err != nil { w.WriteHeader(http.StatusInternalServerError) t.Error(err) return } w.WriteHeader(http.StatusNotFound) }) c, destroy := fakeHTTPClient(h) defer destroy() n := &NeptuneApex{ httpClient: c, } tests := []struct { name string servers []string }{ { name: "Good case, 2 servers", servers: []string{"http://abc", "https://def"}, }, { name: "Good case, 0 servers", }, { name: "Good case nil", servers: nil, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var acc testutil.Accumulator n.Servers = test.servers require.NoError(t, n.Gather(&acc)) require.Lenf(t, acc.Errors, len(test.servers), "Number of servers mismatch. got=%d, want=%d", len(acc.Errors), len(test.servers)) }) } } func TestParseXML(t *testing.T) { goodTime := time.Date(2018, 12, 22, 21, 55, 37, 0, time.FixedZone("PST", 3600*-8)) tests := []struct { name string xmlResponse []byte wantMetrics []telegraf.Metric wantAccErr bool wantErr bool }{ { name: "Good test", xmlResponse: []byte(apex2016), wantMetrics: []telegraf.Metric{ testutil.MustMetric( Measurement, map[string]string{ "source": "apex", "type": "controller", "software": "5.04_7A18", "hardware": "1.0", }, map[string]interface{}{ "serial": "AC5:12345", "power_failed": int64(1544814000000000000), "power_restored": int64(1544833875000000000), }, goodTime, ), testutil.MustMetric( Measurement, map[string]string{ "source": "apex", "output_id": "0", "device_id": "base_Var1", "name": "VarSpd1_I1", "output_type": "variable", "type": "output", "software": "5.04_7A18", "hardware": "1.0", }, map[string]interface{}{"state": "PF1"}, goodTime, ), testutil.MustMetric( Measurement, map[string]string{ "source": "apex", "output_id": "6", "device_id": "base_email", "name": "EmailAlm_I5", "output_type": "alert", "type": "output", "software": "5.04_7A18", "hardware": "1.0", }, map[string]interface{}{"state": "AOF"}, goodTime, ), testutil.MustMetric( Measurement, map[string]string{ "source": "apex", "output_id": "8", "device_id": "2_1", "name": "RETURN_2_1", "output_type": "outlet", "type": "output", "software": "5.04_7A18", "hardware": "1.0", }, map[string]interface{}{ "state": "AON", "watt": 35.0, "amp": 0.3, }, goodTime, ), testutil.MustMetric( Measurement, map[string]string{ "source": "apex", "output_id": "18", "device_id": "3_1", "name": "RVortech_3_1", "output_type": "unknown", "type": "output", "software": "5.04_7A18", "hardware": "1.0", }, map[string]interface{}{ "state": "TBL", "xstatus": "OK", }, goodTime, ), testutil.MustMetric( Measurement, map[string]string{ "source": "apex", "output_id": "28", "device_id": "4_9", "name": "LinkA_4_9", "output_type": "unknown", "type": "output", "software": "5.04_7A18", "hardware": "1.0", }, map[string]interface{}{"state": "AOF"}, goodTime, ), testutil.MustMetric( Measurement, map[string]string{ "source": "apex", "output_id": "32", "device_id": "Cntl_A2", "name": "LEAK", "output_type": "virtual", "type": "output", "software": "5.04_7A18", "hardware": "1.0", }, map[string]interface{}{"state": "AOF"}, goodTime, ), testutil.MustMetric( Measurement, map[string]string{ "source": "apex", "name": "Salt", "type": "probe", "probe_type": "Cond", "software": "5.04_7A18", "hardware": "1.0", }, map[string]interface{}{"value": 30.1}, goodTime, ), testutil.MustMetric( Measurement, map[string]string{ "source": "apex", "name": "Volt_2", "type": "probe", "software": "5.04_7A18", "hardware": "1.0", }, map[string]interface{}{"value": 115.0}, goodTime, ), }, }, { name: "Unmarshal error", xmlResponse: []byte("Invalid"), wantErr: true, }, { name: "Report time failure", xmlResponse: []byte(`abc`), wantErr: true, }, { name: "Power Failed time failure", xmlResponse: []byte( `12/22/2018 21:55:37 -8.0a 12/22/2018 22:55:37`), wantMetrics: []telegraf.Metric{ testutil.MustMetric( Measurement, map[string]string{ "source": "", "type": "controller", "hardware": "", "software": "", }, map[string]interface{}{ "serial": "", "power_restored": int64(1545548137000000000), }, goodTime, ), }, }, { name: "Power restored time failure", xmlResponse: []byte( `12/22/2018 21:55:37 -8.0a 12/22/2018 22:55:37`), wantMetrics: []telegraf.Metric{ testutil.MustMetric( Measurement, map[string]string{ "source": "", "type": "controller", "hardware": "", "software": "", }, map[string]interface{}{ "serial": "", "power_failed": int64(1545548137000000000), }, goodTime, ), }, }, { name: "Power failed failure", xmlResponse: []byte( `abc`), wantErr: true, }, { name: "Failed to parse watt to float", xmlResponse: []byte( ` 12/22/2018 21:55:37-8.0 12/22/2018 21:55:37 12/22/2018 21:55:37 o1 o1Wabc `), wantAccErr: true, wantMetrics: []telegraf.Metric{ testutil.MustMetric( Measurement, map[string]string{ "source": "", "type": "controller", "hardware": "", "software": "", }, map[string]interface{}{ "serial": "", "power_failed": int64(1545544537000000000), "power_restored": int64(1545544537000000000), }, goodTime, ), }, }, { name: "Failed to parse amp to float", xmlResponse: []byte( ` 12/22/2018 21:55:37-8.0 12/22/2018 21:55:37 12/22/2018 21:55:37 o1 o1Aabc `), wantAccErr: true, wantMetrics: []telegraf.Metric{ testutil.MustMetric( Measurement, map[string]string{ "source": "", "type": "controller", "hardware": "", "software": "", }, map[string]interface{}{ "serial": "", "power_failed": int64(1545544537000000000), "power_restored": int64(1545544537000000000), }, goodTime, ), }, }, { name: "Failed to parse probe value to float", xmlResponse: []byte( ` 12/22/2018 21:55:37-8.0 12/22/2018 21:55:37 12/22/2018 21:55:37 p1abc `), wantAccErr: true, wantMetrics: []telegraf.Metric{ testutil.MustMetric( Measurement, map[string]string{ "source": "", "type": "controller", "hardware": "", "software": "", }, map[string]interface{}{ "serial": "", "power_failed": int64(1545544537000000000), "power_restored": int64(1545544537000000000), }, goodTime, ), }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var acc testutil.Accumulator err := parseXML(&acc, test.xmlResponse) if test.wantErr { require.Error(t, err, "expected error but got ") return } // No error case require.NoErrorf(t, err, "expected no error but got: %v", err) require.Equalf(t, test.wantAccErr, len(acc.Errors) > 0, "Accumulator errors. got=%v, want=%t", acc.Errors, test.wantAccErr) testutil.RequireMetricsEqual(t, acc.GetTelegrafMetrics(), test.wantMetrics) }) } } func TestSendRequest(t *testing.T) { t.Parallel() tests := []struct { name string statusCode int wantErr bool }{ { name: "Good case", statusCode: http.StatusOK, }, { name: "Get error", statusCode: http.StatusNotFound, wantErr: true, }, { name: "Status 301", statusCode: http.StatusMovedPermanently, wantErr: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(test.statusCode) if _, err := w.Write([]byte("data")); err != nil { w.WriteHeader(http.StatusInternalServerError) t.Error(err) return } }) c, destroy := fakeHTTPClient(h) defer destroy() n := &NeptuneApex{ httpClient: c, } resp, err := n.sendRequest("http://abc") if test.wantErr { require.Error(t, err, "expected error but got ") return } // No error case require.NoErrorf(t, err, "expected no error but got: %v", err) require.Equalf(t, resp, []byte("data"), "Response data mismatch. got=%q, want=%q", resp, "data") }) } } func TestParseTime(t *testing.T) { t.Parallel() tests := []struct { name string input string timeZone float64 wantTime time.Time wantErr bool }{ { name: "Good case - Timezone positive", input: "01/01/2023 12:34:56", timeZone: 5, wantTime: time.Date(2023, 1, 1, 12, 34, 56, 0, time.FixedZone("a", 3600*5)), }, { name: "Good case - Timezone negative", input: "01/01/2023 12:34:56", timeZone: -8, wantTime: time.Date(2023, 1, 1, 12, 34, 56, 0, time.FixedZone("a", 3600*-8)), }, { name: "Cannot parse", input: "Not a date", wantErr: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() res, err := parseTime(test.input, test.timeZone) if test.wantErr { require.Error(t, err, "expected error but got ") return } // No error case require.NoErrorf(t, err, "expected no error but got: %v", err) require.Truef(t, test.wantTime.Equal(res), "time mismatch. got=%q, want=%q", res, test.wantTime) }) } } func TestFindProbe(t *testing.T) { t.Parallel() fakeProbes := []probe{ { Name: "test1", }, { Name: "good", }, } tests := []struct { name string probeName string wantIndex int }{ { name: "Good case - Found", probeName: "good", wantIndex: 1, }, { name: "Not found", probeName: "bad", wantIndex: -1, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() index := findProbe(test.probeName, fakeProbes) require.Equalf(t, test.wantIndex, index, "probe index mismatch; got=%d, want %d", index, test.wantIndex) }) } } // This fakeHttpClient creates a server and binds a client to it. // That way, it is possible to control the http // output from within the test without changes to the main code. func fakeHTTPClient(h http.Handler) (*http.Client, func()) { s := httptest.NewServer(h) c := &http.Client{ Transport: &http.Transport{ DialContext: func( _ context.Context, network, _ string) (net.Conn, error) { return net.Dial(network, s.Listener.Addr().String()) }, }, } return c, s.Close } // Sample configuration from a 2016 version Neptune Apex. const apex2016 = ` apex AC5:12345 -8.00 12/22/2018 21:55:37 12/14/2018 11:00:00 12/14/2018 16:31:15 Salt 30.1 Cond RETURN_2_1A 0.3 RETURN_2_1W 35 Volt_2 115 VarSpd1_I1 0 PF1 base_Var1 EmailAlm_I5 6 AOF base_email RETURN_2_1 8 AON 2_1 RVortech_3_1 18 TBL 3_1 OK LinkA_4_9 28 AOF 4_9 LEAK 32 AOF Cntl_A2 `