Мастер, QUIK: короткое письмо в ARQA с внешним адресом, памятка администратору, понятный выбор работы с QUIK

- Письмо в ARQA: организация просит подключить модуль неторговых поручений,
  выдать логин и пароль и разрешить подключение с нашего внешнего адреса —
  о bj и внутреннем устройстве ни слова. Внешний адрес bj узнаёт у сервера
  лицензий (GET /v1/ip, раз в час); пометка — убедиться, что адрес постоянный.
- Под письмом — «Памятка администратору QUIK» (классы, логин InstrEu, справочник,
  перезапуск модуля) со ссылкой на подробную инструкцию.
- Вместо свёрнутого «Автоматический цикл M2M — выключен · настроить» и справочника
  под раскрытием: после проверки подключения — «Как bj работает с QUIK»: «Только
  сверка» или «Автоматически» (+ торговый счёт), сохраняется сразу; справочник —
  строкой состояния с напоминанием о перезапуске. До проверки — одна строка.
  Тонкая настройка — в «Настройка → QUIK», мастер её не трогает.
- Проверка МНП: ошибка сертификата (x509, «doesn't contain any IP SANs») —
  подсказка включить «Доверять сертификату сервера», «Дополнительно» раскрывается.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-06 16:59:21 +03:00
parent 8ab3a88c7b
commit a7cfbb2e74
8 changed files with 284 additions and 11 deletions
+86
View File
@@ -462,6 +462,13 @@ func TestDumpOnboardingPages(t *testing.T) {
h.renderWizard(w, httptest.NewRequest(http.MethodGet, fmt.Sprintf("/admin/wizard?step=%d", step), nil))
_ = os.WriteFile(filepath.Join(dir, fmt.Sprintf("wizard-%02d.html", step)), w.Body.Bytes(), 0o644)
}
// шаг QUIK после проверки подключения — выбор «Как bj работает с QUIK»
q := rc.Snapshot().Quik
q.Server, q.User, q.Verified = "91.209.122.88", "BROKER_88", true
_ = rc.UpdateQuik(q)
w := httptest.NewRecorder()
h.renderWizard(w, httptest.NewRequest(http.MethodGet, "/admin/wizard?step=5", nil))
_ = os.WriteFile(filepath.Join(dir, "wizard-05-verified.html"), w.Body.Bytes(), 0o644)
var b strings.Builder
d := PrepareData{page: page{Title: "Подготовка модулей", Active: "wizard"}, Tenant: "АО «Пример Инвест»", Plan: "enterprise", Until: "поддержка до 06.10.2027", Next: "/admin/wizard?step=12", Flash: "Администратор создан."}
if err := a.prepare.ExecuteTemplate(&b, "layout", d); err != nil {
@@ -644,3 +651,82 @@ func TestSaveOrgWithRequisites(t *testing.T) {
t.Fatalf("неверный счёт — ничего не сохраняется: %+v", s)
}
}
// Письмо в ARQA — коротко: организация просит подключить модуль, выдать логин и пароль и разрешить подключение
// с нашего внешнего адреса; о bj и внутреннем устройстве ни слова. Внешний адрес неизвестен — место для него.
func TestArqaLetter(t *testing.T) {
publicIPState.Lock()
publicIPState.ip, publicIPState.at = "195.239.71.22", time.Now()
publicIPState.Unlock()
defer func() {
publicIPState.Lock()
publicIPState.ip, publicIPState.at = "", time.Time{}
publicIPState.Unlock()
}()
var s Settings
s.NSD.OrgName, s.NSD.OrgINN, s.NSD.OrgCode = "ООО \"ИК \"Фонтвьель\"", "7703807489", "MC0413600000"
l, ok := letterFor(s, "arqa")
if !ok {
t.Fatal("нет письма arqa")
}
for _, bad := range []string{"Bridge", "bj", "InstrEu", "sp_NewInstrSec", "M2MDepoRecipients", "10.10."} {
if strings.Contains(l.Body, bad) {
t.Fatalf("в письме лишнее %q:\n%s", bad, l.Body)
}
}
for _, want := range []string{"7703807489", "просит", "логин и пароль", "с нашего адреса 195.239.71.22 [убедитесь, что это постоянный внешний адрес"} {
if !strings.Contains(l.Body, want) {
t.Fatalf("в письме нет %q:\n%s", want, l.Body)
}
}
publicIPState.Lock()
publicIPState.ip = ""
publicIPState.Unlock()
if l, _ := letterFor(s, "arqa"); !strings.Contains(l.Body, "[постоянный внешний IP-адрес вашего сервера") {
t.Fatalf("внешний адрес неизвестен — место для него:\n%s", l.Body)
}
}
// Мастер, QUIK: один выбор вместо набора флажков; тонкая настройка не трогается. Ошибка сертификата — подсказка.
func TestQuikModeAndCertHint(t *testing.T) {
priv := testLicenseKey(t)
rc := testRC(t)
now := time.Now().UTC()
tok := signTestLicense(t, priv, &license.License{Schema: 1, ID: "lic-1", Tenant: "Тест", Product: "bj-server", Plan: license.PlanPro,
IssuedAt: now, ExpiresAt: now.Add(30 * 24 * time.Hour), ContractUntil: now.AddDate(1, 0, 0), Features: []string{"updates", "quik-mnp"}})
if err := rc.SaveLicense(LicenseSettings{Key: tok}); err != nil {
t.Fatal(err)
}
old := globalRC
globalRC = rc
defer func() { globalRC = old }()
q := rc.Snapshot().Quik
q.PollTerminal, q.SecOut, q.ClientCodeMap = true, "RUROUTM2M", map[string]string{"БР-1": "BR1"}
if err := rc.UpdateQuik(q); err != nil {
t.Fatal(err)
}
h := &setupHandlers{rc: rc}
post := func(form url.Values) string {
r := httptest.NewRequest(http.MethodPost, "/admin/setup/quik/mode", strings.NewReader(form.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
r.Header.Set("X-BJ-Ajax", "1")
w := httptest.NewRecorder()
h.saveQuikMode(w, r)
return w.Body.String()
}
if msg := post(url.Values{"mode": {"auto"}, "account": {"L01-00000F00"}}); !strings.Contains(msg, "автоматически") {
t.Fatalf("ответ: %q", msg)
}
s := rc.Snapshot().Quik
if !s.WritesEnabled || !s.AutoOut || !s.AutoIn || s.Account != "L01-00000F00" || !s.PollTerminal || s.SecOut != "RUROUTM2M" || s.ClientCodeMap["БР-1"] != "BR1" {
t.Fatalf("автоматически: %+v", s)
}
post(url.Values{"mode": {"check"}})
if s := rc.Snapshot().Quik; s.WritesEnabled || s.AutoOut || s.AutoIn || s.Account != "L01-00000F00" {
t.Fatalf("только сверка (счёт не стирается): %+v", s)
}
if !quikCertError(errors.New("TLS Handshake failed: tls: failed to verify certificate: x509: cannot validate certificate for 91.209.122.88 because it doesn't contain any IP SANs")) ||
quikCertError(errors.New("mssql: login error: Login failed for user 'BROKER_88'")) {
t.Fatal("ошибка сертификата распознаётся, ошибка логина — нет")
}
}