Files
Bridge-and-Join-s/internal/lkgateway/client_profile_test.go
T
fontvielle 13832fefad Профиль клиента: выгрузка эталона и предзаполнение мастера под клиента
Страница /admin/setup/client-profile: скачать эталон текущей конфигурации (секреты
затёрты) и — для стенда — применить профиль клиента вручную. На боевом профиль
приходит с сервера лицензий (поле config_profile в ответе активации/продления) и
предзаполняет ещё НЕ настроенные разделы мастера, ничего не перетирая. Локальный
фолбэк — файл client-profile.json рядом с setup.json. Администратор правит профиль
под клиента на lic (отличия от стандарта).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XG3zNMMf2LMoDpXo22mwMX
2026-10-16 09:58:23 +03:00

86 lines
2.9 KiB
Go

package lkgateway
import (
"encoding/json"
"strings"
"testing"
)
func TestRedactSecrets(t *testing.T) {
m := map[string]any{
"host": "db",
"password": "p@ss",
"nested": map[string]any{
"dsn": "postgres://u:pw@h/db",
"keep": "ok",
"token": "abc",
},
"list": []any{map[string]any{"api_key": "x", "name": "n"}},
}
redactSecrets(m)
if m["password"] != "" {
t.Errorf("password должен быть затёрт, получили %v", m["password"])
}
n := m["nested"].(map[string]any)
if n["dsn"] != "" || n["token"] != "" {
t.Errorf("dsn/token должны быть затёрты: %v", n)
}
if n["keep"] != "ok" {
t.Errorf("несекретное поле потеряно: %v", n["keep"])
}
if m["list"].([]any)[0].(map[string]any)["api_key"] != "" {
t.Errorf("api_key в массиве должен быть затёрт")
}
}
func TestMergeEmptySections(t *testing.T) {
// Пустой раздел заполняется из профиля.
dst := Settings{}
prof := Settings{Postgres: PostgresSettings{DSN: "postgres://x"}}
filled := mergeEmptySections(&dst, prof)
if dst.Postgres.DSN != "postgres://x" {
t.Fatalf("пустой раздел должен заполниться из профиля, got %q", dst.Postgres.DSN)
}
if len(filled) != 1 || filled[0] != "Postgres" {
t.Fatalf("ожидали filled=[Postgres], got %v", filled)
}
// Настроенный раздел профиль не перетирает.
dst2 := Settings{Postgres: PostgresSettings{DSN: "mine"}}
prof2 := Settings{Postgres: PostgresSettings{DSN: "theirs"}}
filled2 := mergeEmptySections(&dst2, prof2)
if dst2.Postgres.DSN != "mine" {
t.Fatalf("настроенный раздел не должен перетираться, got %q", dst2.Postgres.DSN)
}
if len(filled2) != 0 {
t.Fatalf("ничего не должно заполниться, got %v", filled2)
}
// Лицензия не трогается профилем.
dst3 := Settings{}
prof3 := Settings{License: LicenseSettings{InstallID: "zzz"}}
mergeEmptySections(&dst3, prof3)
if dst3.License.InstallID != "" {
t.Fatalf("лицензия не должна заполняться из профиля, got %q", dst3.License.InstallID)
}
}
func TestExportClientProfileRedacts(t *testing.T) {
rc := &RuntimeConfig{path: t.TempDir() + "/setup.json"}
rc.data = Settings{Postgres: PostgresSettings{DSN: "postgres://u:secret@h/db"}}
body, err := exportClientProfile(rc)
if err != nil {
t.Fatal(err)
}
var m map[string]any
if err := json.Unmarshal(body, &m); err != nil {
t.Fatal(err)
}
s := m["settings"].(map[string]any)
pg := s["postgres"].(map[string]any)
if pg["dsn"] != "" {
t.Errorf("DSN должен быть затёрт в эталоне, got %v", pg["dsn"])
}
if !strings.Contains(string(body), `"settings"`) {
t.Error("эталон должен содержать settings")
}
}