// dsn_test.go — тесты replaceDSNDatabase и safeNamespaceRe. // // Критичны для изоляции тенантов: неверная замена БД = телеметрия всех тенантов // уходит в одну БД; небезопасный namespace = коллизия имени БД/username. package iotpg import ( "strings" "testing" ) func TestReplaceDSNDatabase(t *testing.T) { cases := []struct { name string dsn string newDB string want string wantErr bool }{ { name: "обычный URL-DSN", dsn: "postgresql://u:p@h:5432/iot_platform?sslmode=require", newDB: "tenant_abc", want: "postgresql://u:p@h:5432/tenant_abc?sslmode=require", }, { name: "sslrootcert со слэшем в query не ломает замену", dsn: "postgresql://u:p@h:5432/iot_platform?sslmode=require&sslrootcert=/etc/ssl/ca.pem", newDB: "tenant_abc", want: "postgresql://u:p@h:5432/tenant_abc?sslmode=require&sslrootcert=/etc/ssl/ca.pem", }, { name: "DSN без query", dsn: "postgresql://u:p@h:5432/iot_platform", newDB: "tenant_x", want: "postgresql://u:p@h:5432/tenant_x", }, { name: "key=value DSN — ошибка (иначе смешение тенантов)", dsn: "host=h user=u dbname=x sslmode=disable", wantErr: true, }, { name: "URL без имени БД — ошибка", dsn: "postgresql://u:p@h:5432", wantErr: true, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got, err := replaceDSNDatabase(c.dsn, c.newDB) if c.wantErr { if err == nil { t.Fatalf("ожидалась ошибка, получено %q", got) } return } if err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if got != c.want { t.Errorf("got %q, want %q", got, c.want) } }) } } func TestSafeNamespaceRe(t *testing.T) { cases := []struct { ns string want bool }{ {"myhome", true}, {"sless-abc123", true}, {"a", true}, {"tenant", true}, {"a_b", false}, // _ {"a+b", false}, // + {"a#b", false}, // # {"a/b", false}, // / {"", false}, // пусто {"-x", false}, // ведущий дефис {strings.Repeat("a", 56), true}, {strings.Repeat("a", 57), false}, } for _, c := range cases { if got := safeNamespaceRe.MatchString(c.ns); got != c.want { t.Errorf("safeNamespaceRe(%q) = %v, want %v", c.ns, got, c.want) } } }