modelRT/client/manualsync/client_test.go

241 lines
7.4 KiB
Go

package manualsync
import (
"context"
"encoding/json"
"io"
"net/http"
"strings"
"testing"
"time"
"modelRT/config"
"modelRT/constants"
"modelRT/orm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type capturedRequest struct {
Path string
Method string
ContentType string
Payload Request
}
func TestClientSyncRoutesAndMapsDataSources(t *testing.T) {
cl3611Requests := make(chan capturedRequest, 2)
protocol104Requests := make(chan capturedRequest, 1)
client, err := NewClient(config.ManualSyncConfig{
ProtocolCL3611URL: "http://cl3611.test",
Protocol104URL: "http://protocol104.test",
APIPath: "/api/manual",
Timeout: time.Second,
})
require.NoError(t, err)
client.httpClient.Transport = roundTripFunc(func(request *http.Request) (*http.Response, error) {
var payload Request
require.NoError(t, json.NewDecoder(request.Body).Decode(&payload))
captured := capturedRequest{
Path: request.URL.Path,
Method: request.Method,
ContentType: request.Header.Get("Content-Type"),
Payload: payload,
}
if request.URL.Host == "cl3611.test" {
cl3611Requests <- captured
} else {
protocol104Requests <- captured
}
return httpResponse(http.StatusNoContent, ""), nil
})
tests := []struct {
name string
dataSource orm.JSONMap
requests <-chan capturedRequest
wantTarget Target
}{
{
name: "CL3611 phasor",
dataSource: orm.JSONMap{
"type": 1,
"io_address": map[string]any{
"dtype": 1, "station": "001", "device": "ssu001",
"channel": "TM1", "option": "RMS",
},
},
requests: cl3611Requests,
wantTarget: Target{
Type: 1, Station: "001", MainPos: "ssu001", SubPos: "TM1", Option: "RMS",
},
},
{
name: "CL3611 sample",
dataSource: orm.JSONMap{
"type": 1,
"io_address": map[string]any{
"dtype": 2, "station": "002", "device": "ssu002",
"channel": "TS01", "option": "ignored",
},
},
requests: cl3611Requests,
wantTarget: Target{
Type: 2, Station: "002", MainPos: "ssu002", SubPos: "TS01", Option: "",
},
},
{
name: "104",
dataSource: orm.JSONMap{
"type": 2,
"io_address": map[string]any{
"station": "station000", "packet": 10, "offset": 35,
},
},
requests: protocol104Requests,
wantTarget: Target{
Type: 3, Station: "station000", MainPos: "10", SubPos: "35", Option: "",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := client.Sync(context.Background(), test.dataSource, constants.MeasurementModeAutomatic, nil)
require.NoError(t, err)
captured := <-test.requests
assert.Equal(t, http.MethodPost, captured.Method)
assert.Equal(t, "/api/manual", captured.Path)
assert.Equal(t, "application/json", captured.ContentType)
assert.Equal(t, constants.MeasurementModeAutomatic, captured.Payload.Mode)
assert.Nil(t, captured.Payload.Data)
assert.Equal(t, test.wantTarget, captured.Payload.Target)
})
}
}
func TestClientSyncIncludesManualValueData(t *testing.T) {
requests := make(chan capturedRequest, 1)
client, err := NewClient(config.ManualSyncConfig{
ProtocolCL3611URL: "http://cl3611.test",
Protocol104URL: "http://protocol104.test",
APIPath: "api/manual",
Timeout: time.Second,
})
require.NoError(t, err)
client.httpClient.Transport = captureTransport(t, requests)
sample := SyntheticData{Time: 1736305467506000000, Value: 1.25}
err = client.Sync(context.Background(), orm.JSONMap{
"type": 2,
"io_address": map[string]any{
"station": "station000", "packet": 1, "offset": 2,
},
}, constants.MeasurementModeManual, &sample)
require.NoError(t, err)
captured := <-requests
assert.Equal(t, constants.MeasurementModeManual, captured.Payload.Mode)
assert.Equal(t, []SyntheticData{sample}, captured.Payload.Data)
}
func TestClientSyncRejectsInvalidDataSources(t *testing.T) {
client, err := NewClient(config.ManualSyncConfig{
ProtocolCL3611URL: "http://cl3611.test",
Protocol104URL: "http://protocol104.test",
APIPath: "/api/manual",
Timeout: time.Second,
})
require.NoError(t, err)
client.httpClient.Transport = roundTripFunc(func(*http.Request) (*http.Response, error) {
t.Fatal("invalid data source must not call endpoint")
return nil, nil
})
tests := []struct {
name string
dataSource orm.JSONMap
wantError string
}{
{name: "unsupported type", dataSource: orm.JSONMap{"type": 3, "io_address": map[string]any{"station": "s"}}, wantError: "unsupported"},
{name: "invalid dtype", dataSource: orm.JSONMap{"type": 1, "io_address": map[string]any{"dtype": 3, "station": "s", "device": "d", "channel": "c"}}, wantError: "dtype"},
{name: "missing station", dataSource: orm.JSONMap{"type": 2, "io_address": map[string]any{"packet": 1, "offset": 2}}, wantError: "station"},
{name: "fractional packet", dataSource: orm.JSONMap{"type": 2, "io_address": map[string]any{"station": "s", "packet": 1.5, "offset": 2}}, wantError: "packet"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := client.Sync(context.Background(), test.dataSource, constants.MeasurementModeManual, nil)
require.Error(t, err)
assert.Contains(t, err.Error(), test.wantError)
})
}
}
func TestClientSyncReturnsNonSuccessResponse(t *testing.T) {
client, err := NewClient(config.ManualSyncConfig{
ProtocolCL3611URL: "http://cl3611.test",
Protocol104URL: "http://protocol104.test",
APIPath: "/api/manual",
Timeout: time.Second,
})
require.NoError(t, err)
client.httpClient.Transport = roundTripFunc(func(*http.Request) (*http.Response, error) {
return httpResponse(http.StatusServiceUnavailable, "downstream unavailable"), nil
})
err = client.Sync(context.Background(), orm.JSONMap{
"type": 2,
"io_address": map[string]any{
"station": "s", "packet": 1, "offset": 2,
},
}, constants.MeasurementModeManual, nil)
require.Error(t, err)
assert.Contains(t, err.Error(), "Service Unavailable")
assert.Contains(t, err.Error(), "downstream unavailable")
}
func TestNewClientValidatesConfiguration(t *testing.T) {
_, err := NewClient(config.ManualSyncConfig{})
require.Error(t, err)
assert.Contains(t, err.Error(), "timeout")
_, err = NewClient(config.ManualSyncConfig{
ProtocolCL3611URL: "127.0.0.1:9001",
Protocol104URL: "http://127.0.0.1:9002",
APIPath: "/api/manual",
Timeout: time.Second,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid protocol CL3611 URL")
}
type roundTripFunc func(*http.Request) (*http.Response, error)
func (roundTrip roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
return roundTrip(request)
}
func captureTransport(t *testing.T, requests chan<- capturedRequest) http.RoundTripper {
t.Helper()
return roundTripFunc(func(request *http.Request) (*http.Response, error) {
var payload Request
require.NoError(t, json.NewDecoder(request.Body).Decode(&payload))
requests <- capturedRequest{
Path: request.URL.Path,
Method: request.Method,
ContentType: request.Header.Get("Content-Type"),
Payload: payload,
}
return httpResponse(http.StatusNoContent, ""), nil
})
}
func httpResponse(status int, body string) *http.Response {
return &http.Response{
StatusCode: status,
Status: http.StatusText(status),
Body: io.NopCloser(strings.NewReader(body)),
Header: make(http.Header),
}
}