package handler import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "modelRT/common" "modelRT/constants" "modelRT/orm" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestParseDataObjectAttributeQuery(t *testing.T) { const token = "nspath.component.bay.measurement" tests := []struct { name string target string wantToken string wantField string wantErr string }{ { name: "reads token and field from query parameters", target: "/data-object/attribute?token=" + token + "&field=NAME", wantToken: token, wantField: "name", }, { name: "defaults missing field to value", target: "/data-object/attribute?token=" + token, wantToken: token, wantField: "value", }, { name: "defaults empty field to value", target: "/data-object/attribute?token=" + token + "&field=", wantToken: token, wantField: "value", }, { name: "rejects missing token", target: "/data-object/attribute?field=value", wantErr: "token is missing from query parameters", }, { name: "rejects empty token", target: "/data-object/attribute?token=&field=value", wantErr: "token is missing from query parameters", }, { name: "does not read legacy path parameters", target: "/data-object/attribute/" + token + "/value", wantErr: "token is missing from query parameters", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) ctx.Request = httptest.NewRequest(http.MethodGet, tt.target, nil) token, field, err := parseDataObjectAttributeQuery(ctx) if tt.wantErr != "" { require.EqualError(t, err, tt.wantErr) assert.Empty(t, token) assert.Empty(t, field) return } require.NoError(t, err) assert.Equal(t, tt.wantToken, token) assert.Equal(t, tt.wantField, field) }) } } func TestValidateDataObjectField(t *testing.T) { tests := []struct { name string token string dataObjectType constants.DataObjectType field string wantErr error }{ {name: "bay value", token: "nspath.component.bay.measurement", dataObjectType: constants.DataObjectTypeMeasurement, field: "value"}, {name: "bay name", token: "nspath.component.bay.measurement", dataObjectType: constants.DataObjectTypeMeasurement, field: "name"}, {name: "bay binding", token: "nspath.component.bay.measurement", dataObjectType: constants.DataObjectTypeMeasurement, field: "binding"}, {name: "bay field is case insensitive", token: "nspath.component.bay.measurement", dataObjectType: constants.DataObjectTypeMeasurement, field: "DATA_SOURCE"}, {name: "bay unsupported", token: "nspath.component.bay.measurement", dataObjectType: constants.DataObjectTypeMeasurement, field: "unknown", wantErr: common.ErrUnsupportedMeasurementField}, {name: "parameter value", token: "nspath.component.rated.attribute", dataObjectType: constants.DataObjectTypeParameter, field: "value"}, {name: "parameter name", token: "nspath.component.rated.attribute", dataObjectType: constants.DataObjectTypeParameter, field: "name"}, {name: "parameter rejects mode", token: "nspath.component.rated.attribute", dataObjectType: constants.DataObjectTypeParameter, field: "mode", wantErr: common.ErrUnsupportedParameterField}, {name: "component name", token: "nspath.component.component.name", dataObjectType: constants.DataObjectTypeParameter, field: "name"}, {name: "component rejects mode", token: "nspath.component.component.name", dataObjectType: constants.DataObjectTypeParameter, field: "mode", wantErr: common.ErrUnsupportedParameterField}, {name: "parameter rejects size", token: "nspath.component.rated.attribute", dataObjectType: constants.DataObjectTypeParameter, field: "size", wantErr: common.ErrUnsupportedParameterField}, {name: "invalid type", token: "token", dataObjectType: constants.DataObjectType("unknown"), field: "value", wantErr: assert.AnError}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateDataObjectField(tt.dataObjectType, tt.field) if tt.wantErr == nil { require.NoError(t, err) return } require.Error(t, err) if tt.wantErr != assert.AnError { assert.ErrorIs(t, err, tt.wantErr) } }) } } func TestQueryParameterAttributeValueFromRedisHash(t *testing.T) { fields := map[string]string{ "value": "220.50", "type": "DOUBLE PRECISION", "name": "110kV_TV.cable_22.rated.rated_voltage", "description": "额定电压", } loader := hashFieldLoaderForTest(fields) value, err := queryDataObjectAttributeValue( context.Background(), constants.DataObjectTypeParameter, "parameter-token", "value", loader, nil, nil, ) require.NoError(t, err) assert.Equal(t, json.Number("220.50"), value) description, err := queryDataObjectAttributeValue( context.Background(), constants.DataObjectTypeParameter, "parameter-token", "description", loader, nil, nil, ) require.NoError(t, err) assert.Equal(t, "额定电压", description) } func TestQueryMeasurementAttributeValueFromRedisHash(t *testing.T) { fields := map[string]string{ "mode": "1", "size": "10", "name": "110kV_TV.IA_rms", "data_source": `{"type":1,"io_address":{"channel":"tm1p"}}`, "event_plan": `{"enabled":true}`, } loader := hashFieldLoaderForTest(fields) mode, err := queryDataObjectAttributeValue( context.Background(), constants.DataObjectTypeMeasurement, "measurement-token", "mode", loader, nil, nil, ) require.NoError(t, err) assert.Equal(t, int16(1), mode) eventPlan, err := queryDataObjectAttributeValue( context.Background(), constants.DataObjectTypeMeasurement, "measurement-token", "event_plan", loader, nil, nil, ) require.NoError(t, err) assert.Equal(t, map[string]any{"enabled": true}, eventPlan) } func TestQueryMeasurementRealtimeValueUsesDataSourceFromRedisHash(t *testing.T) { dataSource := orm.JSONMap{ "type": float64(1), "io_address": map[string]any{ "station": "001", "channel": "tm1p", }, } metadataLoader := func(_ context.Context, token string) (orm.JSONMap, int, error) { assert.Equal(t, "measurement-token", token) return dataSource, 2, nil } valueLoader := func(_ context.Context, dataSource orm.JSONMap, size int) (any, error) { assert.Equal(t, float64(1), dataSource["type"]) assert.Equal(t, "001", dataSource["io_address"].(map[string]any)["station"]) assert.Equal(t, 2, size) return []float64{220, 219.5}, nil } value, err := queryDataObjectAttributeValue( context.Background(), constants.DataObjectTypeMeasurement, "measurement-token", "value", nil, metadataLoader, valueLoader, ) require.NoError(t, err) assert.Equal(t, []float64{220, 219.5}, value) } func TestQueryDataObjectAttributeValuePropagatesRedisTokenNotFound(t *testing.T) { loader := func(context.Context, constants.DataObjectType, string, string) (string, error) { return "", fmt.Errorf("%w: token", common.ErrParameterTokenNotFound) } _, err := queryDataObjectAttributeValue( context.Background(), constants.DataObjectTypeParameter, "missing-token", "name", loader, nil, nil, ) require.Error(t, err) assert.ErrorIs(t, err, common.ErrParameterTokenNotFound) assert.True(t, isDataObjectTokenNotFound(err)) } func TestDecodeParameterHashValue(t *testing.T) { tests := []struct { name string rawValue string attributeType string expected any }{ {name: "boolean", rawValue: "true", attributeType: "BOOLEAN", expected: true}, {name: "integer", rawValue: "42", attributeType: "INTEGER", expected: int64(42)}, {name: "numeric", rawValue: "1234567890.123456789", attributeType: "NUMERIC(30,9)", expected: json.Number("1234567890.123456789")}, {name: "jsonb", rawValue: `{"key":"value"}`, attributeType: "JSONB", expected: map[string]any{"key": "value"}}, {name: "string", rawValue: "cable", attributeType: "CHARACTER VARYING(64)", expected: "cable"}, {name: "null", rawValue: "null", attributeType: "INTEGER", expected: nil}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual, err := decodeParameterHashValue(test.rawValue, test.attributeType) require.NoError(t, err) assert.Equal(t, test.expected, actual) }) } } func hashFieldLoaderForTest(fields map[string]string) dataObjectHashFieldLoader { return func(_ context.Context, _ constants.DataObjectType, _ string, field string) (string, error) { value, exists := fields[field] if !exists { return "", fmt.Errorf("field %q not found", field) } return value, nil } }