622 lines
15 KiB
Go
622 lines
15 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>`),
|
|
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(
|
|
`<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>`),
|
|
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(
|
|
`<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>
|
|
`
|