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: "boolean automatic mode", field: "mode", value: `true`, expected: true}, {name: "string manual mode", field: "mode", value: `"false"`, expected: false}, {name: "invalid mode", field: "mode", value: `"manual"`, 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 TestValidateDataObjectAttributeUpdateRejectsShortMeasurementToken(t *testing.T) { _, _, _, err := validateDataObjectAttributeUpdate(dataObjectAttributeUpdateRequest{ Token: "nspath.measurement", Field: "value", Value: json.RawMessage(`1`), }) require.Error(t, err) assert.Contains(t, err.Error(), "token6=bay") } func TestValidateDataObjectAttributeUpdateRequiredFields(t *testing.T) { tests := []struct { name string request dataObjectAttributeUpdateRequest }{ {name: "missing token", request: dataObjectAttributeUpdateRequest{Field: "value", Value: json.RawMessage(`1`)}}, {name: "missing field", request: dataObjectAttributeUpdateRequest{Token: "nspath.measurement", 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 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 = \$5`). WithArgs(int16(1), 500, 500, sqlmock.AnyArg(), int64(10)). WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectRollback() result, err := updateMeasurementDataObject(context.Background(), tx, "nspath.measurement", "mode", true, nil, nil) require.NoError(t, err) assert.Contains(t, result.message, "automatic") 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", true, nil, nil) require.NoError(t, err) assert.Contains(t, result.message, "already") require.NoError(t, tx.Rollback().Error) require.NoError(t, mock.ExpectationsWereMet()) } 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, nil) 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 = \$4`). WithArgs(500, 500, sqlmock.AnyArg(), 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 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), writer, 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), writer, nil) 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(`(?s)SELECT \* 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)) }