test(iot): юнит-тесты валидации идентификаторов и разбора DSN (P1-11)

- handler/devices_test.go: validateIdentifier (charset/лимиты, запрет _ + # /).
- iotpg/dsn_test.go: replaceDSNDatabase (вкл. sslrootcert со слэшем, ошибка для
  key=value) + safeNamespaceRe.
- gofmt для затронутых файлов. go test ./... зелёный.
This commit is contained in:
Repinoid
2026-10-10 19:23:09 +03:00
parent 3a73207abd
commit 7c80c7b7f8
4 changed files with 146 additions and 13 deletions
+12 -12
View File
@@ -42,19 +42,19 @@ type iotDeviceUpdateRequest struct {
// устройства (отдаётся один раз) и НИКОГДА при чтении — чтобы пароль не утекал
// через GET и не хранился в логах/истории.
type iotDeviceResponse struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
DeviceID string `json:"device_id"`
Enabled bool `json:"enabled"`
Phase string `json:"phase"`
MQTTUsername string `json:"mqtt_username,omitempty"`
MQTTPassword string `json:"mqtt_password,omitempty"`
TopicPrefix string `json:"topic_prefix,omitempty"`
Name string `json:"name"`
Namespace string `json:"namespace"`
DeviceID string `json:"device_id"`
Enabled bool `json:"enabled"`
Phase string `json:"phase"`
MQTTUsername string `json:"mqtt_username,omitempty"`
MQTTPassword string `json:"mqtt_password,omitempty"`
TopicPrefix string `json:"topic_prefix,omitempty"`
// TelemetryTopic — полный MQTT-топик телеметрии устройства «ns/telemetry/device».
// Явное поле (в отличие от topic_prefix = «ns/»): Terraform должен знать
// точный топик для публикации/подписки (ревью 2026-10-10).
TelemetryTopic string `json:"telemetry_topic"`
LastConnected string `json:"last_connected,omitempty"`
TelemetryTopic string `json:"telemetry_topic"`
LastConnected string `json:"last_connected,omitempty"`
// Metadata — без omitempty: всегда присутствует в ответе. Пустой → «{}»,
// иначе Terraform видел бы drift «{}» против null (ревью 2026-10-10).
Metadata map[string]string `json:"metadata"`
@@ -127,8 +127,8 @@ func hashPassword(plain string) (string, error) {
// только [a-zA-Z0-9] и дефисы, не с дефиса. Исключает _, +, #, /, пробелы, юникод.
// Причины запретов (ревью 2026-10-10):
// - _ — username = «namespace_device_id»; _ делает его неоднозначным
// (auth-запрос EMQX: namespace || '_' || device_id, LIMIT 1 без ORDER BY
// → может сматчить чужое устройство);
// (auth-запрос EMQX: namespace || '_' || device_id, LIMIT 1 без ORDER BY
// → может сматчить чужое устройство);
// - +/# — wildcard MQTT: в ACL-топике даст подписку/публикацию чужих топиков;
// - / — ломает топик «ns/telemetry/device» (лишний уровень).
var identRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]*$`)
@@ -0,0 +1,42 @@
// devices_test.go — тесты валидации идентификаторов (validateIdentifier).
//
// Покрывают charset/лимиты, критичные для MQTT (username/topic) и PG (имя БД):
// запрет _, +, #, /, ведущего дефиса, пробелов и превышения длины.
package handler
import (
"strings"
"testing"
)
func TestValidateIdentifier(t *testing.T) {
cases := []struct {
name string
value string
maxLen int
wantOK bool
}{
{"простое имя", "boiler", 64, true},
{"дефис внутри", "boiler-1", 64, true},
{"одна буква", "a", 64, true},
{"цифры", "dev001", 64, true},
{"подчёркивание запрещено", "a_b", 64, false},
{"плюс запрещён (wildcard MQTT)", "a+b", 64, false},
{"решётка запрещена (wildcard MQTT)", "a#b", 64, false},
{"слэш запрещён (уровень топика)", "a/b", 64, false},
{"ведущий дефис", "-abc", 64, false},
{"пробел", "abc def", 64, false},
{"пусто", "", 64, false},
{"юникод", "имя", 64, false},
{"ровно максимум", strings.Repeat("a", 56), 56, true},
{"больше максимума", strings.Repeat("a", 57), 56, false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
gotOK := validateIdentifier(c.value, "x", c.maxLen) == ""
if gotOK != c.wantOK {
t.Errorf("validateIdentifier(%q, max=%d) ok=%v, want %v", c.value, c.maxLen, gotOK, c.wantOK)
}
})
}
}