telegraf/plugins/inputs/neptune_apex/neptune_apex_test.go

592 lines
14 KiB
Go

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(`<status><date>abc</date></status>`),
wantErr: true,
},
{
name: "Power Failed time failure",
xmlResponse: []byte(
`<status><date>12/22/2018 21:55:37</date>
<timezone>-8.0</timezone><power><failed>a</failed>
<restored>12/22/2018 22:55:37</restored></power></status>`),
wantErr: true,
},
{
name: "Power restored time failure",
xmlResponse: []byte(
`<status><date>12/22/2018 21:55:37</date>
<timezone>-8.0</timezone><power><restored>a</restored>
<failed>12/22/2018 22:55:37</failed></power></status>`),
wantErr: true,
},
{
name: "Power failed failure",
xmlResponse: []byte(
`<status><power><failed>abc</failed></power></status>`),
wantErr: true,
},
{
name: "Failed to parse watt to float",
xmlResponse: []byte(
`<?xml version="1.0"?><status>
<date>12/22/2018 21:55:37</date><timezone>-8.0</timezone>
<power><failed>12/22/2018 21:55:37</failed>
<restored>12/22/2018 21:55:37</restored></power>
<outlets><outlet><name>o1</name></outlet></outlets>
<probes><probe><name>o1W</name><value>abc</value></probe>
</probes></status>`),
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(
`<?xml version="1.0"?><status>
<date>12/22/2018 21:55:37</date><timezone>-8.0</timezone>
<power><failed>12/22/2018 21:55:37</failed>
<restored>12/22/2018 21:55:37</restored></power>
<outlets><outlet><name>o1</name></outlet></outlets>
<probes><probe><name>o1A</name><value>abc</value></probe>
</probes></status>`),
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(
`<?xml version="1.0"?><status>
<date>12/22/2018 21:55:37</date><timezone>-8.0</timezone>
<power><failed>12/22/2018 21:55:37</failed>
<restored>12/22/2018 21:55:37</restored></power>
<probes><probe><name>p1</name><value>abc</value></probe>
</probes></status>`),
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 = `<?xml version="1.0"?>
<status software="5.04_7A18" hardware="1.0">
<hostname>apex</hostname>
<serial>AC5:12345</serial>
<timezone>-8.00</timezone>
<date>12/22/2018 21:55:37</date>
<power><failed>12/14/2018 11:00:00</failed>
<restored>12/14/2018 16:31:15</restored></power>
<probes>
<probe>
<name>Salt</name> <value>30.1 </value>
<type>Cond</type></probe><probe>
<name>RETURN_2_1A</name> <value>0.3 </value>
</probe><probe>
<name>RETURN_2_1W</name> <value> 35 </value>
</probe><probe>
<name>Volt_2</name> <value>115 </value>
</probe></probes>
<outlets>
<outlet>
<name>VarSpd1_I1</name>
<outputID>0</outputID>
<state>PF1</state>
<deviceID>base_Var1</deviceID>
</outlet>
<outlet>
<name>EmailAlm_I5</name>
<outputID>6</outputID>
<state>AOF</state>
<deviceID>base_email</deviceID>
</outlet>
<outlet>
<name>RETURN_2_1</name>
<outputID>8</outputID>
<state>AON</state>
<deviceID>2_1</deviceID>
</outlet>
<outlet>
<name>RVortech_3_1</name>
<outputID>18</outputID>
<state>TBL</state>
<deviceID>3_1</deviceID>
<xstatus>OK</xstatus></outlet>
<outlet>
<name>LinkA_4_9</name>
<outputID>28</outputID>
<state>AOF</state>
<deviceID>4_9</deviceID>
</outlet>
<outlet>
<name>LEAK</name>
<outputID>32</outputID>
<state>AOF</state>
<deviceID>Cntl_A2</deviceID>
</outlet>
</outlets></status>
`