package neptuneapex import ( "bytes" "context" "net" "net/http" "net/http/httptest" "reflect" "testing" "time" "github.com/influxdata/telegraf/testutil" ) func TestGather(t *testing.T) { h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) w.Write([]byte("data")) }) 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", servers: []string{}, }, { name: "Good case nil", servers: nil, }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { var acc testutil.Accumulator n.Servers = test.servers n.Gather(&acc) if len(acc.Errors) != len(test.servers) { t.Errorf("Number of servers mismatch. got=%d, want=%d", len(acc.Errors), len(test.servers)) } }) } } func TestParseXML(t *testing.T) { n := &NeptuneApex{} goodTime := time.Date(2018, 12, 22, 21, 55, 37, 0, time.FixedZone("PST", 3600*-8)) tests := []struct { name string xmlResponse []byte wantMetrics []*testutil.Metric wantAccErr bool wantErr bool }{ { name: "Good test", xmlResponse: []byte(APEX2016), wantMetrics: []*testutil.Metric{ { Measurement: Measurement, Time: goodTime, Tags: map[string]string{ "source": "apex", "type": "controller", "software": "5.04_7A18", "hardware": "1.0", }, Fields: map[string]interface{}{ "serial": "AC5:12345", "power_failed": int64(1544814000000000000), "power_restored": int64(1544833875000000000), }, }, { Measurement: Measurement, Time: goodTime, Tags: 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", }, Fields: map[string]interface{}{"state": "PF1"}, }, { Measurement: Measurement, Time: goodTime, Tags: 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", }, Fields: map[string]interface{}{"state": "AOF"}, }, { Measurement: Measurement, Time: goodTime, Tags: 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", }, Fields: map[string]interface{}{ "state": "AON", "watt": 35.0, "amp": 0.3, }, }, { Measurement: Measurement, Time: goodTime, Tags: 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", }, Fields: map[string]interface{}{ "state": "TBL", "xstatus": "OK", }, }, { Measurement: Measurement, Time: goodTime, Tags: 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", }, Fields: map[string]interface{}{"state": "AOF"}, }, { Measurement: Measurement, Time: goodTime, Tags: 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", }, Fields: map[string]interface{}{"state": "AOF"}, }, { Measurement: Measurement, Time: goodTime, Tags: map[string]string{ "source": "apex", "name": "Salt", "type": "probe", "probe_type": "Cond", "software": "5.04_7A18", "hardware": "1.0", }, Fields: map[string]interface{}{"value": 30.1}, }, { Measurement: Measurement, Time: goodTime, Tags: map[string]string{ "source": "apex", "name": "Volt_2", "type": "probe", "software": "5.04_7A18", "hardware": "1.0", }, Fields: map[string]interface{}{"value": 115.0}, }, }, }, { 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: []*testutil.Metric{ { Measurement: Measurement, Time: goodTime, Tags: map[string]string{ "source": "", "type": "controller", "hardware": "", "software": "", }, Fields: map[string]interface{}{ "serial": "", "power_restored": int64(1545548137000000000), }, }, }, }, { name: "Power restored time failure", xmlResponse: []byte( `12/22/2018 21:55:37 -8.0a 12/22/2018 22:55:37`), wantMetrics: []*testutil.Metric{ { Measurement: Measurement, Time: goodTime, Tags: map[string]string{ "source": "", "type": "controller", "hardware": "", "software": "", }, Fields: map[string]interface{}{ "serial": "", "power_failed": int64(1545548137000000000), }, }, }, }, { 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: []*testutil.Metric{ { Measurement: Measurement, Time: goodTime, Tags: map[string]string{ "source": "", "type": "controller", "hardware": "", "software": "", }, Fields: map[string]interface{}{ "serial": "", "power_failed": int64(1545544537000000000), "power_restored": int64(1545544537000000000), }, }, }, }, { 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: []*testutil.Metric{ { Measurement: Measurement, Time: goodTime, Tags: map[string]string{ "source": "", "type": "controller", "hardware": "", "software": "", }, Fields: map[string]interface{}{ "serial": "", "power_failed": int64(1545544537000000000), "power_restored": int64(1545544537000000000), }, }, }, }, { 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: []*testutil.Metric{ { Measurement: Measurement, Time: goodTime, Tags: map[string]string{ "source": "", "type": "controller", "hardware": "", "software": "", }, Fields: map[string]interface{}{ "serial": "", "power_failed": int64(1545544537000000000), "power_restored": int64(1545544537000000000), }, }, }, }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { var acc testutil.Accumulator err := n.parseXML(&acc, []byte(test.xmlResponse)) if (err != nil) != test.wantErr { t.Errorf("err mismatch. got=%v, want=%t", err, test.wantErr) } if test.wantErr { return } if len(acc.Errors) > 0 != test.wantAccErr { t.Errorf("Accumulator errors. got=%v, want=none", acc.Errors) } if len(acc.Metrics) != len(test.wantMetrics) { t.Fatalf("Invalid number of metrics received. got=%d, want=%d", len(acc.Metrics), len(test.wantMetrics)) } for i, m := range acc.Metrics { if m.Measurement != test.wantMetrics[i].Measurement { t.Errorf("Metric measurement mismatch at position %d:\ngot=\n%s\nWant=\n%s", i, m.Measurement, test.wantMetrics[i].Measurement) } if !reflect.DeepEqual(m.Tags, test.wantMetrics[i].Tags) { t.Errorf("Metric tags mismatch at position %d:\ngot=\n%v\nwant=\n%v", i, m.Tags, test.wantMetrics[i].Tags) } if !reflect.DeepEqual(m.Fields, test.wantMetrics[i].Fields) { t.Errorf("Metric fields mismatch at position %d:\ngot=\n%#v\nwant=:\n%#v", i, m.Fields, test.wantMetrics[i].Fields) } if !m.Time.Equal(test.wantMetrics[i].Time) { t.Errorf("Metric time mismatch at position %d:\ngot=\n%s\nwant=\n%s", i, m.Time, test.wantMetrics[i].Time) } } }) } } func TestSendRequest(t *testing.T) { 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 { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() h := http.HandlerFunc(func( w http.ResponseWriter, r *http.Request) { w.WriteHeader(test.statusCode) w.Write([]byte("data")) }) c, destroy := fakeHTTPClient(h) defer destroy() n := &NeptuneApex{ httpClient: c, } resp, err := n.sendRequest("http://abc") if (err != nil) != test.wantErr { t.Errorf("err mismatch. got=%v, want=%t", err, test.wantErr) } if test.wantErr { return } if bytes.Compare(resp, []byte("data")) != 0 { t.Errorf( "Response data mismatch. got=%q, want=%q", resp, "data") } }) } } func TestParseTime(t *testing.T) { 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 { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() res, err := parseTime(test.input, test.timeZone) if (err != nil) != test.wantErr { t.Errorf("err mismatch. got=%v, want=%t", err, test.wantErr) } if test.wantErr { return } if !test.wantTime.Equal(res) { t.Errorf("err mismatch. got=%s, want=%s", res, test.wantTime) } }) } } func TestFindProbe(t *testing.T) { 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 { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() index := findProbe(test.probeName, fakeProbes) if index != test.wantIndex { t.Errorf("probe index mismatch; got=%d, want %d", index, test.wantIndex) } }) } } func TestDescription(t *testing.T) { n := &NeptuneApex{} if n.Description() == "" { t.Errorf("Empty description") } } func TestSampleConfig(t *testing.T) { n := &NeptuneApex{} if n.SampleConfig() == "" { t.Errorf("Empty sample config") } } // 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 `