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), } }