package database import ( "context" "errors" "fmt" "regexp" "strings" "testing" "modelRT/common" "github.com/DATA-DOG/go-sqlmock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/postgres" "gorm.io/gorm" ) func TestBuildMeasurementTokenValidationQuery(t *testing.T) { tests := []struct { name string token string wantArgs []any wantWhere string wantErr bool }{ { name: "seven-part token", token: "grid.zone.station.nspath.component.bay.measurement", wantArgs: []any{"grid", "zone", "station", "nspath", "component", "measurement"}, wantWhere: "WHERE g.tagname = ?", }, { name: "four-part token", token: "nspath.component.bay.measurement", wantArgs: []any{"nspath", "component", "measurement"}, wantWhere: "WHERE c.nspath = ?", }, { name: "two-part token", token: "nspath.measurement", wantArgs: []any{"nspath", "measurement"}, wantWhere: "WHERE c.nspath = ?", }, { name: "non-bay group", token: "nspath.component.rated.attribute", wantErr: true, }, { name: "empty segment", token: "nspath..bay.measurement", wantErr: true, }, { name: "invalid segment count", token: "grid.zone.station", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { query, args, err := buildMeasurementTokenValidationQuery(tt.token) if tt.wantErr { require.Error(t, err) assert.ErrorIs(t, err, common.ErrInvalidMeasurementToken) return } require.NoError(t, err) assert.Contains(t, query, "INNER JOIN component AS c ON c.global_uuid = m.component_uuid") assert.Contains(t, query, "INNER JOIN bay AS b ON b.bay_uuid = m.bay_uuid") assert.Contains(t, query, tt.wantWhere) assert.NotContains(t, query, "grid_idWHERE") assert.Regexp(t, `grid_id\s+WHERE`, query) assert.NotContains(t, query, "\n") assert.NotContains(t, query, "\t") assert.Equal(t, tt.wantArgs, args) }) } } func TestValidateMeasurementToken(t *testing.T) { tests := []struct { name string count int64 queryErr error wantErr error }{ {name: "exists", count: 1}, {name: "not found", count: 0, wantErr: common.ErrMeasurementTokenNotFound}, {name: "ambiguous", count: 2, wantErr: common.ErrAmbiguousMeasurementToken}, {name: "query failure", queryErr: errors.New("database unavailable")}, } const token = "nspath.measurement" query, _, err := buildMeasurementTokenValidationQuery(token) require.NoError(t, err) expectedQuery := query for i := 1; strings.Contains(expectedQuery, "?"); i++ { expectedQuery = strings.Replace(expectedQuery, "?", fmt.Sprintf("$%d", i), 1) } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sqlDB, mock, err := sqlmock.New() require.NoError(t, err) t.Cleanup(func() { _ = sqlDB.Close() }) db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{}) require.NoError(t, err) expectation := mock.ExpectQuery(regexp.QuoteMeta(expectedQuery)). WithArgs("nspath", "measurement") if tt.queryErr != nil { expectation.WillReturnError(tt.queryErr) } else { expectation.WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(tt.count)) } err = ValidateMeasurementToken(context.Background(), db, token) if tt.wantErr != nil { assert.ErrorIs(t, err, tt.wantErr) } else if tt.queryErr != nil { require.Error(t, err) assert.Contains(t, err.Error(), tt.queryErr.Error()) } else { require.NoError(t, err) } require.NoError(t, mock.ExpectationsWereMet()) }) } } func TestBuildMeasurementRowsQuerySeparatesLimitClause(t *testing.T) { validationQuery, _, err := buildMeasurementTokenValidationQuery("nspath.measurement") require.NoError(t, err) query := buildMeasurementRowsQuery(validationQuery) assert.NotContains(t, query, "?LIMIT") assert.Regexp(t, `m\.tag = \?\s+LIMIT 2$`, query) assert.NotContains(t, query, "\n") assert.NotContains(t, query, "\t") } func TestQueryMeasurementByDataObjectToken(t *testing.T) { sqlDB, mock, err := sqlmock.New() require.NoError(t, err) t.Cleanup(func() { _ = sqlDB.Close() }) db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{}) require.NoError(t, err) 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", "name", "mode", "size", "data_source", "event_plan", "binding", "component_uuid", }).AddRow( int64(10), "measurement", "A phase current", int16(1), 10, `{"type":1,"io_address":{"channel":"tm1"}}`, `{"enabled":true}`, `{"ct":{"ratio":1}}`, 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", "grid", "zone", "station", }).AddRow(componentUUID, "nspath", "component", "grid", "zone", "station")) measurement, component, err := QueryMeasurementByDataObjectToken(context.Background(), db, "nspath.measurement") require.NoError(t, err) assert.Equal(t, int64(10), measurement.ID) assert.Equal(t, int16(1), measurement.Mode) assert.Equal(t, float64(1), measurement.DataSource["type"]) assert.Equal(t, "grid", component.GridName) assert.Equal(t, "component", component.Tag) require.NoError(t, mock.ExpectationsWereMet()) }