475 lines
17 KiB
Go
475 lines
17 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"modelRT/common/errcode"
|
|
"modelRT/constants"
|
|
"modelRT/orm"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func TestValidateDataObjectAttributeUpdateParameterGroups(t *testing.T) {
|
|
groups := []string{
|
|
"rated",
|
|
"setup",
|
|
"model",
|
|
"stable",
|
|
"craft",
|
|
"integrity",
|
|
"behavior",
|
|
}
|
|
|
|
for _, group := range groups {
|
|
t.Run(group, func(t *testing.T) {
|
|
request := dataObjectAttributeUpdateRequest{
|
|
Token: fmt.Sprintf("nspath.component.%s.attribute", group),
|
|
Field: "VALUE",
|
|
Value: json.RawMessage(`"15.2"`),
|
|
}
|
|
|
|
dataObjectType, field, value, err := validateDataObjectAttributeUpdate(request)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, constants.DataObjectTypeParameter, dataObjectType)
|
|
assert.Equal(t, "value", field)
|
|
assert.Equal(t, "15.2", value)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateDataObjectAttributeUpdateRejectsUnsupportedParameterGroups(t *testing.T) {
|
|
for _, group := range []string{"component", "base_extend"} {
|
|
t.Run(group, func(t *testing.T) {
|
|
_, _, _, err := validateDataObjectAttributeUpdate(dataObjectAttributeUpdateRequest{
|
|
Token: fmt.Sprintf("nspath.component.%s.attribute", group),
|
|
Field: "value",
|
|
Value: json.RawMessage(`"uuid"`),
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "do not support token6="+group)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateDataObjectAttributeUpdateMeasurementFields(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
field string
|
|
value string
|
|
expected any
|
|
wantError bool
|
|
}{
|
|
{name: "numeric value", field: "value", value: `15.2`, expected: float64(15.2)},
|
|
{name: "numeric string value", field: "value", value: `"15.2"`, expected: float64(15.2)},
|
|
{name: "automatic mode", field: "mode", value: `1`, expected: constants.MeasurementModeAutomatic},
|
|
{name: "manual mode", field: "mode", value: `0`, expected: constants.MeasurementModeManual},
|
|
{name: "boolean mode is rejected", field: "mode", value: `true`, wantError: true},
|
|
{name: "out of range mode", field: "mode", value: `2`, wantError: true},
|
|
{name: "unsupported field", field: "name", value: `"measurement"`, wantError: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dataObjectType, field, value, err := validateDataObjectAttributeUpdate(dataObjectAttributeUpdateRequest{
|
|
Token: "nspath.component.bay.measurement",
|
|
Field: tt.field,
|
|
Value: json.RawMessage(tt.value),
|
|
})
|
|
if tt.wantError {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
assert.Equal(t, constants.DataObjectTypeMeasurement, dataObjectType)
|
|
assert.Equal(t, tt.field, field)
|
|
assert.Equal(t, tt.expected, value)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateDataObjectAttributeUpdateAcceptsToken4Token7Measurement(t *testing.T) {
|
|
dataObjectType, field, value, err := validateDataObjectAttributeUpdate(dataObjectAttributeUpdateRequest{
|
|
Token: "nspath.measurement",
|
|
Value: json.RawMessage(`15.2`),
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, constants.DataObjectTypeMeasurement, dataObjectType)
|
|
assert.Equal(t, "value", field)
|
|
assert.Equal(t, float64(15.2), value)
|
|
}
|
|
|
|
func TestValidateDataObjectAttributeUpdateRequiredFields(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
request dataObjectAttributeUpdateRequest
|
|
}{
|
|
{name: "missing token", request: dataObjectAttributeUpdateRequest{Field: "value", Value: json.RawMessage(`1`)}},
|
|
{name: "missing value", request: dataObjectAttributeUpdateRequest{Token: "nspath.measurement", Field: "value"}},
|
|
{name: "null value", request: dataObjectAttributeUpdateRequest{Token: "nspath.measurement", Field: "value", Value: json.RawMessage(`null`)}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, _, _, err := validateDataObjectAttributeUpdate(tt.request)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateDataObjectAttributeUpdateDefaultsEmptyFieldToValue(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
request dataObjectAttributeUpdateRequest
|
|
wantType constants.DataObjectType
|
|
wantValue any
|
|
}{
|
|
{
|
|
name: "parameter omitted field",
|
|
request: dataObjectAttributeUpdateRequest{
|
|
Token: "nspath.component.rated.attribute",
|
|
Value: json.RawMessage(`"15.2"`),
|
|
},
|
|
wantType: constants.DataObjectTypeParameter,
|
|
wantValue: "15.2",
|
|
},
|
|
{
|
|
name: "measurement whitespace field",
|
|
request: dataObjectAttributeUpdateRequest{
|
|
Token: "nspath.component.bay.measurement",
|
|
Field: " ",
|
|
Value: json.RawMessage(`15.2`),
|
|
},
|
|
wantType: constants.DataObjectTypeMeasurement,
|
|
wantValue: float64(15.2),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dataObjectType, field, value, err := validateDataObjectAttributeUpdate(tt.request)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.wantType, dataObjectType)
|
|
assert.Equal(t, "value", field)
|
|
assert.Equal(t, tt.wantValue, value)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMeasurementModeIsAutomatic(t *testing.T) {
|
|
automatic, err := measurementModeIsAutomatic(constants.MeasurementModeAutomatic)
|
|
require.NoError(t, err)
|
|
assert.True(t, automatic)
|
|
|
|
automatic, err = measurementModeIsAutomatic(constants.MeasurementModeManual)
|
|
require.NoError(t, err)
|
|
assert.False(t, automatic)
|
|
|
|
_, err = measurementModeIsAutomatic(-1)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "invalid mode")
|
|
}
|
|
|
|
func TestUpdateMeasurementDataObjectLocksRowAndUpdatesMode(t *testing.T) {
|
|
db, mock, closeDB := newDataObjectUpdateTestDB(t)
|
|
defer closeDB()
|
|
|
|
mock.ExpectBegin()
|
|
tx := db.Begin()
|
|
require.NoError(t, tx.Error)
|
|
expectMeasurementResolution(mock, 0)
|
|
mock.ExpectExec(`UPDATE "measurement" SET .*"mode"=\$1.*"operations"=.*WHERE id = \$4`).
|
|
WithArgs(constants.MeasurementModeAutomatic, sqlmock.AnyArg(), 500, int64(10)).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectRollback()
|
|
|
|
startCalled := false
|
|
result, err := updateMeasurementDataObject(context.Background(), tx, "nspath.measurement", "mode", constants.MeasurementModeAutomatic, nil, measurementUpdateDependencies{
|
|
startDataRTFunc: func(_ context.Context, dataSource orm.JSONMap, value *float64) error {
|
|
startCalled = true
|
|
assert.Equal(t, float64(1), dataSource["type"])
|
|
assert.Nil(t, value)
|
|
return nil
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, startCalled)
|
|
assert.Contains(t, result.message, "automatic")
|
|
require.NoError(t, tx.Rollback().Error)
|
|
require.NoError(t, mock.ExpectationsWereMet())
|
|
}
|
|
|
|
func TestUpdateMeasurementModeToAutomaticReturnsErrorWhenDataRTStartFails(t *testing.T) {
|
|
db, mock, closeDB := newDataObjectUpdateTestDB(t)
|
|
defer closeDB()
|
|
|
|
mock.ExpectBegin()
|
|
tx := db.Begin()
|
|
require.NoError(t, tx.Error)
|
|
expectMeasurementResolution(mock, constants.MeasurementModeManual)
|
|
mock.ExpectExec(`UPDATE "measurement" SET .*"mode"=\$1.*"operations"=.*WHERE id = \$4`).
|
|
WithArgs(constants.MeasurementModeAutomatic, sqlmock.AnyArg(), 500, int64(10)).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectRollback()
|
|
|
|
_, err := updateMeasurementDataObject(context.Background(), tx, "nspath.measurement", "mode", constants.MeasurementModeAutomatic, nil, measurementUpdateDependencies{
|
|
startDataRTFunc: func(context.Context, orm.JSONMap, *float64) error {
|
|
return fmt.Errorf("dataRT unavailable")
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "start automatic measurement write")
|
|
require.NoError(t, tx.Rollback().Error)
|
|
require.NoError(t, mock.ExpectationsWereMet())
|
|
}
|
|
|
|
func TestUpdateMeasurementDataObjectReturnsMessageWhenModeIsUnchanged(t *testing.T) {
|
|
db, mock, closeDB := newDataObjectUpdateTestDB(t)
|
|
defer closeDB()
|
|
|
|
mock.ExpectBegin()
|
|
tx := db.Begin()
|
|
require.NoError(t, tx.Error)
|
|
expectMeasurementResolution(mock, 1)
|
|
mock.ExpectRollback()
|
|
|
|
result, err := updateMeasurementDataObject(context.Background(), tx, "nspath.measurement", "mode", constants.MeasurementModeAutomatic, nil, measurementUpdateDependencies{})
|
|
require.NoError(t, err)
|
|
assert.Contains(t, result.message, "already")
|
|
require.NoError(t, tx.Rollback().Error)
|
|
require.NoError(t, mock.ExpectationsWereMet())
|
|
}
|
|
|
|
func TestUpdateMeasurementModeToManualWithoutDataOnlyStopsDataRT(t *testing.T) {
|
|
db, mock, closeDB := newDataObjectUpdateTestDB(t)
|
|
defer closeDB()
|
|
|
|
mock.ExpectBegin()
|
|
tx := db.Begin()
|
|
require.NoError(t, tx.Error)
|
|
expectMeasurementResolution(mock, constants.MeasurementModeAutomatic)
|
|
mock.ExpectExec(`UPDATE "measurement" SET .*"mode"=\$1.*"operations"=.*WHERE id = \$4`).
|
|
WithArgs(constants.MeasurementModeManual, sqlmock.AnyArg(), 500, int64(10)).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectRollback()
|
|
|
|
stopCalled := false
|
|
replaceCalled := false
|
|
result, err := updateMeasurementDataObject(context.Background(), tx, "nspath.measurement", "mode", constants.MeasurementModeManual, nil, measurementUpdateDependencies{
|
|
updateDataRTFunc: func(_ context.Context, dataSource orm.JSONMap, value *float64) error {
|
|
stopCalled = true
|
|
assert.Equal(t, float64(1), dataSource["type"])
|
|
assert.Nil(t, value)
|
|
return nil
|
|
},
|
|
replaceRedisValueFunc: func(context.Context, *orm.Measurement, float64) error {
|
|
replaceCalled = true
|
|
return nil
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, stopCalled)
|
|
assert.False(t, replaceCalled)
|
|
assert.Contains(t, result.message, "manual")
|
|
require.NoError(t, tx.Rollback().Error)
|
|
require.NoError(t, mock.ExpectationsWereMet())
|
|
}
|
|
|
|
func TestUpdateMeasurementModeToManualReplacesRedisValueWhenDataProvided(t *testing.T) {
|
|
db, mock, closeDB := newDataObjectUpdateTestDB(t)
|
|
defer closeDB()
|
|
|
|
mock.ExpectBegin()
|
|
tx := db.Begin()
|
|
require.NoError(t, tx.Error)
|
|
expectMeasurementResolution(mock, constants.MeasurementModeAutomatic)
|
|
mock.ExpectExec(`UPDATE "measurement" SET .*"mode"=\$1.*"operations"=.*WHERE id = \$4`).
|
|
WithArgs(constants.MeasurementModeManual, sqlmock.AnyArg(), 500, int64(10)).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectRollback()
|
|
|
|
callOrder := make([]string, 0, 2)
|
|
result, err := updateMeasurementDataObject(context.Background(), tx, "nspath.measurement", "mode", constants.MeasurementModeManual, json.RawMessage(`0`), measurementUpdateDependencies{
|
|
updateDataRTFunc: func(_ context.Context, _ orm.JSONMap, value *float64) error {
|
|
callOrder = append(callOrder, "stop-dataRT")
|
|
assert.Nil(t, value)
|
|
return nil
|
|
},
|
|
replaceRedisValueFunc: func(_ context.Context, measurement *orm.Measurement, value float64) error {
|
|
callOrder = append(callOrder, "replace-redis")
|
|
assert.Equal(t, int64(10), measurement.ID)
|
|
assert.Equal(t, float64(0), value)
|
|
return nil
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"stop-dataRT", "replace-redis"}, callOrder)
|
|
assert.Contains(t, result.message, "manual")
|
|
require.NoError(t, tx.Rollback().Error)
|
|
require.NoError(t, mock.ExpectationsWereMet())
|
|
}
|
|
|
|
func TestUpdateMeasurementModeToManualDoesNotTouchRedisWhenDataRTStopFails(t *testing.T) {
|
|
db, mock, closeDB := newDataObjectUpdateTestDB(t)
|
|
defer closeDB()
|
|
|
|
mock.ExpectBegin()
|
|
tx := db.Begin()
|
|
require.NoError(t, tx.Error)
|
|
expectMeasurementResolution(mock, constants.MeasurementModeAutomatic)
|
|
mock.ExpectExec(`UPDATE "measurement" SET .*"mode"=\$1.*"operations"=.*WHERE id = \$4`).
|
|
WithArgs(constants.MeasurementModeManual, sqlmock.AnyArg(), 500, int64(10)).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectRollback()
|
|
|
|
replaceCalled := false
|
|
_, err := updateMeasurementDataObject(context.Background(), tx, "nspath.measurement", "mode", constants.MeasurementModeManual, json.RawMessage(`15.2`), measurementUpdateDependencies{
|
|
updateDataRTFunc: func(_ context.Context, _ orm.JSONMap, value *float64) error {
|
|
assert.Nil(t, value)
|
|
return fmt.Errorf("dataRT unavailable")
|
|
},
|
|
replaceRedisValueFunc: func(context.Context, *orm.Measurement, float64) error {
|
|
replaceCalled = true
|
|
return nil
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "stop automatic measurement write")
|
|
assert.False(t, replaceCalled)
|
|
require.NoError(t, tx.Rollback().Error)
|
|
require.NoError(t, mock.ExpectationsWereMet())
|
|
}
|
|
|
|
func TestParseOptionalMeasurementModeData(t *testing.T) {
|
|
for _, raw := range []json.RawMessage{nil, json.RawMessage(`null`)} {
|
|
value, err := parseOptionalMeasurementModeData(raw)
|
|
require.NoError(t, err)
|
|
assert.Nil(t, value)
|
|
}
|
|
|
|
value, err := parseOptionalMeasurementModeData(json.RawMessage(`"15.2"`))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, value)
|
|
assert.Equal(t, 15.2, *value)
|
|
|
|
_, err = parseOptionalMeasurementModeData(json.RawMessage(`"invalid"`))
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestUpdateMeasurementDataObjectRejectsValueInAutomaticMode(t *testing.T) {
|
|
db, mock, closeDB := newDataObjectUpdateTestDB(t)
|
|
defer closeDB()
|
|
|
|
mock.ExpectBegin()
|
|
tx := db.Begin()
|
|
require.NoError(t, tx.Error)
|
|
expectMeasurementResolution(mock, 1)
|
|
mock.ExpectRollback()
|
|
|
|
_, err := updateMeasurementDataObject(context.Background(), tx, "nspath.measurement", "value", float64(15.2), nil, measurementUpdateDependencies{})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "read-only")
|
|
require.NoError(t, tx.Rollback().Error)
|
|
require.NoError(t, mock.ExpectationsWereMet())
|
|
}
|
|
|
|
func TestUpdateMeasurementDataObjectWritesValueInManualMode(t *testing.T) {
|
|
db, mock, closeDB := newDataObjectUpdateTestDB(t)
|
|
defer closeDB()
|
|
|
|
mock.ExpectBegin()
|
|
tx := db.Begin()
|
|
require.NoError(t, tx.Error)
|
|
expectMeasurementResolution(mock, 0)
|
|
mock.ExpectExec(`UPDATE "measurement" SET "operations"=.*WHERE id = \$3`).
|
|
WithArgs(sqlmock.AnyArg(), 500, int64(10)).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectRollback()
|
|
|
|
called := false
|
|
writer := func(_ context.Context, measurement *orm.Measurement, value float64) error {
|
|
called = true
|
|
assert.Equal(t, int64(10), measurement.ID)
|
|
assert.Equal(t, float64(15.2), value)
|
|
return nil
|
|
}
|
|
dataRTCalled := false
|
|
dataRTWriter := func(_ context.Context, dataSource orm.JSONMap, value *float64) error {
|
|
dataRTCalled = true
|
|
require.NotNil(t, value)
|
|
assert.Equal(t, float64(15.2), *value)
|
|
assert.Equal(t, float64(1), dataSource["type"])
|
|
return nil
|
|
}
|
|
result, err := updateMeasurementDataObject(context.Background(), tx, "nspath.measurement", "value", float64(15.2), nil, measurementUpdateDependencies{
|
|
writeManualValueFunc: writer,
|
|
updateDataRTFunc: dataRTWriter,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, called)
|
|
assert.True(t, dataRTCalled)
|
|
assert.Contains(t, result.message, "updated")
|
|
assert.False(t, result.recordFailure)
|
|
require.NoError(t, tx.Rollback().Error)
|
|
require.NoError(t, mock.ExpectationsWereMet())
|
|
}
|
|
|
|
func TestUpdateMeasurementDataObjectReturnsFailureResultAndAppError(t *testing.T) {
|
|
db, mock, closeDB := newDataObjectUpdateTestDB(t)
|
|
defer closeDB()
|
|
|
|
mock.ExpectBegin()
|
|
tx := db.Begin()
|
|
require.NoError(t, tx.Error)
|
|
expectMeasurementResolution(mock, 0)
|
|
mock.ExpectRollback()
|
|
|
|
writeErr := fmt.Errorf("write value failed")
|
|
writer := func(context.Context, *orm.Measurement, float64) error { return writeErr }
|
|
result, err := updateMeasurementDataObject(context.Background(), tx, "nspath.measurement", "value", float64(15.2), nil, measurementUpdateDependencies{
|
|
writeManualValueFunc: writer,
|
|
})
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, errcode.ErrMeasurementValueUpdateFailed)
|
|
assert.ErrorIs(t, err, writeErr)
|
|
assert.True(t, result.recordFailure)
|
|
assert.Equal(t, int64(10), result.measurementID)
|
|
assert.Equal(t, float64(15.2), result.value)
|
|
require.NoError(t, tx.Rollback().Error)
|
|
require.NoError(t, mock.ExpectationsWereMet())
|
|
}
|
|
|
|
func newDataObjectUpdateTestDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, func()) {
|
|
t.Helper()
|
|
sqlDB, mock, err := sqlmock.New()
|
|
require.NoError(t, err)
|
|
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{SkipDefaultTransaction: true})
|
|
require.NoError(t, err)
|
|
return db, mock, func() { _ = sqlDB.Close() }
|
|
}
|
|
|
|
func expectMeasurementResolution(mock sqlmock.Sqlmock, mode int16) {
|
|
const componentUUID = "70c190f2-8a60-42a9-b143-ec5f87e0aa6b"
|
|
mock.ExpectQuery(`(?s)SELECT m\.\*.*WHERE c\.nspath = \$1.*AND m\.tag = \$2.*LIMIT 2`).
|
|
WithArgs("nspath", "measurement").
|
|
WillReturnRows(sqlmock.NewRows([]string{
|
|
"id", "tag", "mode", "data_source", "component_uuid",
|
|
}).AddRow(int64(10), "measurement", mode, `{"type":1,"io_address":{"station":"station","device":"device","channel":"tm1"}}`, componentUUID))
|
|
mock.ExpectQuery(`(?s)SELECT global_uuid, nspath, tag, grid, zone, station.*WHERE global_uuid = \$1.*LIMIT 1`).
|
|
WithArgs(componentUUID).
|
|
WillReturnRows(sqlmock.NewRows([]string{"global_uuid", "nspath", "tag"}).
|
|
AddRow(componentUUID, "nspath", "component"))
|
|
mock.ExpectQuery(`SELECT "id","mode","data_source" FROM "measurement" WHERE id = \$1 LIMIT \$2 FOR UPDATE`).
|
|
WithArgs(int64(10), 1).
|
|
WillReturnRows(sqlmock.NewRows([]string{
|
|
"id", "tag", "mode", "data_source", "component_uuid",
|
|
}).AddRow(int64(10), "measurement", mode, `{"type":1,"io_address":{"station":"station","device":"device","channel":"tm1"}}`, componentUUID))
|
|
}
|