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
`