Files
Bridge-and-Join-s/modules/lk/lk_test.go
T

509 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lk
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// fakeBJ — API bj-server для кабинета: проверка ключа, вход и портфель.
func fakeBJ(t *testing.T) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
keyOK := func(w http.ResponseWriter, r *http.Request) bool {
if r.Header.Get("X-BJ-Key") != "lk-key" {
w.WriteHeader(http.StatusForbidden)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "неверный ключ модуля"})
return false
}
return true
}
mux.HandleFunc("/api/lk/login", func(w http.ResponseWriter, r *http.Request) {
if keyOK(w, r) {
_ = json.NewEncoder(w).Encode(map[string]any{"session": "sess-1", "full_name": "Мостовиков Тест", "is_admin": false})
}
})
mux.HandleFunc("/api/lk/portfolio", func(w http.ResponseWriter, r *http.Request) {
if keyOK(w, r) && r.Header.Get("X-BJ-Session") == "sess-1" {
_ = json.NewEncoder(w).Encode(map[string]any{"full_name": "Мостовиков Тест", "contract": "4519", "source": "депозитарный учёт", "total_value": 110268.0, "priced": 1, "positions": []any{
map[string]any{"isin": "RU0009029540", "security_name": "Сбербанк, ао", "account_id": "ВТНКЦ00000001", "quantity": 400, "blocked": 100, "available": 300, "transferable": true, "updated_at": "17.09.2026 21:58", "price": 275.67, "value": 110268, "change_pct": -1.53},
map[string]any{"isin": "US0378331005", "security_name": "Apple Inc.", "account_id": "В000000044", "quantity": 12, "blocked": 0, "available": 0, "transferable": false, "note": "иностранная бумага — перевод через M2M недоступен"},
}})
}
})
mux.HandleFunc("/api/lk/claims", func(w http.ResponseWriter, r *http.Request) {
if keyOK(w, r) && r.Header.Get("X-BJ-Session") == "sess-1" {
_ = json.NewEncoder(w).Encode(map[string]any{"claims": []any{
map[string]any{"guid": "41523F7F-1111", "status": "sent", "status_ru": "Отправлено в НРД", "created_at": "16.09.2026", "securities": 1},
map[string]any{"guid": "D744A410-2222", "status": "done", "status_ru": "Завершена", "created_at": "09.09.2026", "securities": 2},
}})
}
})
mux.HandleFunc("/api/lk/security", func(w http.ResponseWriter, r *http.Request) {
if keyOK(w, r) && r.Header.Get("X-BJ-Session") == "sess-1" {
_ = json.NewEncoder(w).Encode(map[string]any{"security": map[string]any{
"isin": "RU0009029540", "name": "Сбербанк РФ-АО3", "kind": "акция", "issuer": "ПАО Сбербанк",
"reg_number": "10301481B", "cfi": "ESVXFR", "face": 3, "accepted": true, "price": 275.67, "price_in": "₽ за штуку",
"board": "TQBR", "change_pct": -1.53,
"position": map[string]any{"isin": "RU0009029540", "quantity": 400, "available": 300, "value": 110268, "transferable": true},
"payments": []any{map[string]any{"date": "18.10.2026", "kind": "дивиденды", "amount": 34.5}},
}})
}
})
mux.HandleFunc("/api/lk/operations", func(w http.ResponseWriter, r *http.Request) {
if keyOK(w, r) && r.Header.Get("X-BJ-Session") == "sess-1" {
_ = json.NewEncoder(w).Encode(map[string]any{"operations": []any{
map[string]any{"date": "16.09.2026", "kind": "зачисление", "number": "260915/1", "account": "ВТНКЦ00000001",
"isin": "RU0009029540", "security": "Сбербанк, ао", "quantity": 100, "state": "исполнено", "note": "Отчёт клиринговой организации"},
map[string]any{"date": "14.08.2026", "kind": "списание", "account": "ВТНКЦ00000001", "isin": "RU000A1014L8",
"security": "Ликвидность, пай", "quantity": -120, "state": "исполнено"},
}})
}
})
mux.HandleFunc("/api/lk/income", func(w http.ResponseWriter, r *http.Request) {
if keyOK(w, r) && r.Header.Get("X-BJ-Session") == "sess-1" {
_ = json.NewEncoder(w).Encode(map[string]any{"accrued": 540.6, "paid": 494.6, "tax": 46.0, "income": []any{
map[string]any{"date": "02.06.2026", "kind": "дивиденды", "isin": "RU000A106YF0", "security": "ВК, ао",
"quantity": 5, "accrued": 45, "tax": 6, "paid": 39},
map[string]any{"date": "03.06.2026", "kind": "купон", "isin": "RU000A1038V6", "security": "ОФЗ 26238",
"accrued": 495.6, "tax": 0, "paid": 495.6},
}})
}
})
mux.HandleFunc("/api/lk/ping", func(w http.ResponseWriter, r *http.Request) {
if keyOK(w, r) {
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
}
})
mux.HandleFunc("/api/lk/portfolio2", func(w http.ResponseWriter, r *http.Request) {
if !keyOK(w, r) {
return
}
view := r.URL.Query().Get("view")
asOf := "18.09.2026"
if view == "archive" {
asOf = "31.12.2025"
}
_ = json.NewEncoder(w).Encode(map[string]any{"contract": "BR-1", "full_name": "Мостовиков Тест", "portfolio": map[string]any{
"source": map[bool]string{true: "spectre", false: "quik"}[view == "archive"], "as_of": asOf,
"total": 110268.0, "securities_value": 92698.28, "cash_rub": 17569.72, "cost": 80000.0, "pnl": 12698.28, "pnl_pct": 15.87, "priced": 1,
"cash": []map[string]any{{"currency": "SUR", "amount": 17569.72}},
"positions": []map[string]any{{"isin": "RU0009029540", "name": "Сбербанк, ао", "kind": "акция", "quantity": 336, "avg_price": 238.1, "price": 275.887, "value": 92698.28, "cost": 80000, "pnl": 12698.28, "pnl_pct": 15.87, "day_pct": 1.2, "share": 84.1}},
}})
})
mux.HandleFunc("/api/lk/ops", func(w http.ResponseWriter, r *http.Request) {
if !keyOK(w, r) {
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"from": "2025-09-18", "to": "2026-09-18", "operations": []map[string]any{
{"date": "10.07.2026", "kind": "продажа", "side": "out", "name": "НОВАТЭК 001P-03", "isin": "RU000A10AUX8", "quantity": 1700, "price": 7793.07, "amount": 13248214.78, "currency": "SUR", "amount_rub": 13248214.78},
{"date": "06.03.2026", "kind": "покупка", "side": "in", "name": "Сбербанк, ао", "isin": "RU0009029540", "quantity": 100, "price": 238.1, "amount": 23810, "currency": "SUR", "amount_rub": 23810},
{"date": "12.05.2026", "kind": "дивиденды", "side": "in", "name": "Сбербанк, ао", "isin": "RU0009029540", "quantity": 336, "amount": 494.6, "currency": "SUR", "amount_rub": 494.6},
}})
})
mux.HandleFunc("/api/lk/admin/bootstrap", func(w http.ResponseWriter, r *http.Request) {
if !keyOK(w, r) {
return
}
var in struct{ Login, Password string }
_ = json.NewDecoder(r.Body).Decode(&in)
if in.Login == "" || len(in.Password) < 10 {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "пароль минимум 10 символов"})
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "message": "администратор создан"})
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
return srv
}
func TestLoginPortfolioHealth(t *testing.T) {
bj := fakeBJ(t)
s, err := New(Config{BJURL: bj.URL + "/", BJKey: "lk-key", Title: "Кабинет · тест"})
if err != nil {
t.Fatal(err)
}
h := s.Handler()
// Форма входа подписана токеном CSRF: без него запрос не выполняется.
csrf := csrfFromPage(t, h)
form := url.Values{"login": {"client"}, "password": {"secret"}, "csrf": {csrf.Value}}
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(csrf)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/" {
t.Fatalf("вход: код %d, Location %q", rec.Code, rec.Header().Get("Location"))
}
var sess *http.Cookie
for _, c := range rec.Result().Cookies() {
if c.Name == "bj_lk" {
sess = c
}
}
if sess == nil || sess.Value != "sess-1" || !sess.HttpOnly {
t.Fatalf("кука сессии: %+v", sess)
}
req = httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sess)
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
body := rec.Body.String()
if rec.Code != http.StatusOK || !strings.Contains(body, "Мостовиков Тест") || !strings.Contains(body, "Кабинет · тест") {
t.Fatalf("портфель: код %d: %s", rec.Code, body)
}
if strings.Contains(body, `href="/clients"`) {
t.Error("клиенту без признака администратора показан раздел «Клиенты»")
}
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/health", nil))
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"ok":true`) {
t.Fatalf("health: код %d: %s", rec.Code, rec.Body.String())
}
}
// Признак администратора берётся из кук своего запроса, а не предыдущего (прежде был общий curReq).
func TestAdminFlagPerRequest(t *testing.T) {
bj := fakeBJ(t)
s, err := New(Config{BJURL: bj.URL, BJKey: "lk-key"})
if err != nil {
t.Fatal(err)
}
h := s.Handler()
admin := httptest.NewRequest(http.MethodGet, "/login", nil)
admin.AddCookie(&http.Cookie{Name: "bj_lk_admin", Value: "1"})
h.ServeHTTP(httptest.NewRecorder(), admin)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(&http.Cookie{Name: "bj_lk", Value: "sess-1"})
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if strings.Contains(rec.Body.String(), `href="/clients"`) {
t.Fatal("признак администратора перешёл из чужого запроса")
}
}
// Кабинет без настроек ведёт в веб-установщик, а /health честно отвечает, что связи нет.
func TestNotConfigured(t *testing.T) {
s, err := New(Config{BJURL: "http://127.0.0.1:8080"})
if err != nil {
t.Fatal(err)
}
s.SetDataDir(t.TempDir())
if s.Configured() {
t.Fatal("без ключа кабинет не настроен")
}
h := s.Handler()
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/install" {
t.Fatalf("/: код %d, Location %q — ожидали установщик", rec.Code, rec.Header().Get("Location"))
}
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/install", nil))
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "Проверить связь и сохранить") {
t.Fatalf("/install: код %d", rec.Code)
}
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/health", nil))
if rec.Code != http.StatusBadGateway {
t.Fatalf("/health без настроек: код %d", rec.Code)
}
}
// Веб-установщик целиком: проверка сервера → связь с ядром → администратор → файл настроек.
func TestInstaller(t *testing.T) {
bj := fakeBJ(t)
dir := t.TempDir()
s, err := New(Config{})
if err != nil {
t.Fatal(err)
}
s.SetDataDir(dir)
h := s.Handler()
csrf := csrfFromPage(t, h)
post := func(form url.Values) *httptest.ResponseRecorder {
form.Set("csrf", csrf.Value)
req := httptest.NewRequest(http.MethodPost, "/install", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(csrf)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
// неверный ключ — настройки не сохраняются
rec := post(url.Values{"step": {"connect"}, "mode": {"direct"}, "bj_url": {bj.URL}, "bj_key": {"чужой"}})
if !strings.Contains(rec.Body.String(), "Связь с ядром не установлена") || s.Configured() {
t.Fatalf("неверный ключ принят: %d", rec.Code)
}
// верный ключ — сохранено, шаг администратора
rec = post(url.Values{"step": {"connect"}, "mode": {"direct"}, "bj_url": {bj.URL}, "bj_key": {"lk-key"}, "title": {"Тест"}})
if !strings.Contains(rec.Body.String(), "Администратор кабинета") || !s.Configured() {
t.Fatalf("связь не сохранена: %d %s", rec.Code, rec.Body.String()[:200])
}
raw, err := os.ReadFile(filepath.Join(dir, configFileName))
if err != nil || !strings.Contains(string(raw), `"bj_key": "lk-key"`) || !strings.Contains(string(raw), `"mode": "direct"`) {
t.Fatalf("файл настроек: %v %s", err, raw)
}
if st, _ := os.Stat(filepath.Join(dir, configFileName)); st.Mode().Perm() != 0o600 {
t.Fatalf("права файла настроек %o", st.Mode().Perm())
}
// администратор: пароли не совпали → ошибка; совпали → ядро получило bootstrap
rec = post(url.Values{"step": {"admin"}, "login": {"admin"}, "password": {"Надёжный-пароль-1"}, "password2": {"другой"}})
if !strings.Contains(rec.Body.String(), "Пароли не совпадают") {
t.Fatal("несовпадение паролей пропущено")
}
rec = post(url.Values{"step": {"admin"}, "login": {"admin"}, "password": {"Надёжный-пароль-1"}, "password2": {"Надёжный-пароль-1"}})
if !strings.Contains(rec.Body.String(), "Кабинет установлен") {
t.Fatalf("завершение: %d %s", rec.Code, rec.Body.String()[:200])
}
// установщик закрыт, настройки поднимаются из файла при новом запуске
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/install", nil))
if !strings.Contains(rec.Body.String(), "уже настроен") {
t.Fatal("установщик остался открытым после установки")
}
cfg, err := LoadConfigFile(dir, Config{})
if err != nil || cfg.BJKey != "lk-key" || cfg.BJURL != bj.URL || cfg.Agent || cfg.Title != "Тест" {
t.Fatalf("настройки из файла: %+v %v", cfg, err)
}
// окружение перекрывает файл
cfg, _ = LoadConfigFile(dir, Config{BJKey: "из-окружения"})
if cfg.BJKey != "из-окружения" {
t.Fatal("окружение должно перекрывать файл")
}
}
// Страницы кабинета в оформлении «Табло»: обзор, реестр, поручения, профиль и бланк поручения.
func TestBoardPages(t *testing.T) {
bj := fakeBJ(t)
s, err := New(Config{BJURL: bj.URL, BJKey: "lk-key", Title: "Кабинет"})
if err != nil {
t.Fatal(err)
}
h := s.Handler()
get := func(path string) string {
req := httptest.NewRequest(http.MethodGet, path, nil)
req.AddCookie(&http.Cookie{Name: "bj_lk", Value: "sess-1"})
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("%s: код %d", path, rec.Code)
}
return rec.Body.String()
}
board := get("/")
for _, want := range []string{"Стоимость портфеля", "Новое поручение", "Сбербанк, ао", "Онлайн", "Архив", "Денежные средства", "12 698,28 ₽", "15,87 %", "84,1%"} {
if !strings.Contains(board, want) {
t.Errorf("в портфеле нет %q", want)
if i := strings.Index(board, "Результат"); i > 0 {
t.Logf("фрагмент: %s", board[i:min(i+400, len(board))])
}
}
}
if !strings.Contains(board, "110 268 ₽") {
t.Errorf("стоимость портфеля не показана: %s", board)
}
arch := get("/?view=archive&date=2025-12-31")
if !strings.Contains(arch, "на 31.12.2025") || !strings.Contains(arch, `value="2025-12-31"`) {
t.Errorf("архив на дату не показан")
}
// старые адреса ведут на новые разделы
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/portfolio", nil))
if rec.Code != http.StatusMovedPermanently || rec.Header().Get("Location") != "/" {
t.Errorf("/portfolio должен вести на портфель: %d %s", rec.Code, rec.Header().Get("Location"))
}
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/income", nil))
if rec.Code != http.StatusMovedPermanently || rec.Header().Get("Location") != "/operations" {
t.Errorf("/income должен вести на операции: %d %s", rec.Code, rec.Header().Get("Location"))
}
orders := get("/orders")
if !strings.Contains(orders, "Поручения депозитарию") || !strings.Contains(orders, "Перевод в другой депозитарий") {
t.Errorf("журнал поручений: %s", orders)
}
order := get("/order?isin=RU0009029540")
for _, want := range []string{"Поручение депозитарию", "Вид поручения", "ИНН принимающего депозитария", `value="RU0009029540" id="cRU0009029540" checked`} {
if !strings.Contains(order, want) {
t.Errorf("в бланке поручения нет %q", want)
}
}
card := get("/security?isin=RU0009029540")
for _, want := range []string{"ПАО Сбербанк", "275,67", "дивиденды", "Перевести эту бумагу", "← Портфель"} {
if !strings.Contains(card, want) {
t.Errorf("в карточке бумаги нет %q", want)
}
}
ops := get("/operations")
for _, want := range []string{"Операции", "продажа", "покупка", "дивиденды", "НОВАТЭК 001P-03", "−13 248 214,78 ₽", "+494,60 ₽", "Продажи"} {
if !strings.Contains(ops, want) {
t.Errorf("в операциях нет %q", want)
}
}
prof := get("/profile")
if !strings.Contains(prof, "Счета депо") || !strings.Contains(prof, "ВТНКЦ00000001") {
t.Errorf("профиль: %s", prof)
}
// старые адреса продолжают работать
for path, to := range map[string]string{"/transfer": "/order", "/claims": "/orders"} {
req := httptest.NewRequest(http.MethodGet, path, nil)
req.AddCookie(&http.Cookie{Name: "bj_lk", Value: "sess-1"})
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusMovedPermanently || rec.Header().Get("Location") != to {
t.Errorf("%s → %s: код %d, Location %q", path, to, rec.Code, rec.Header().Get("Location"))
}
}
}
// csrfFromPage — открыть страницу входа и взять с неё токен формы.
func csrfFromPage(t *testing.T, h http.Handler) *http.Cookie {
t.Helper()
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/login", nil))
if loc := rec.Header().Get("Location"); rec.Code == http.StatusSeeOther && loc != "" {
rec = httptest.NewRecorder() // кабинет не настроен: токен выдаёт установщик
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, loc, nil))
}
for _, c := range rec.Result().Cookies() {
if c.Name == csrfCookie {
return c
}
}
t.Fatal("страница входа не выдала токен формы")
return nil
}
// TestSecurityHeaders — кабинет отдаёт заголовки защиты и не разрешает кэшировать страницы с данными.
func TestSecurityHeaders(t *testing.T) {
bj := fakeBJ(t)
s, err := New(Config{BJURL: bj.URL, BJKey: "lk-key"})
if err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
s.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/login", nil))
for h, want := range map[string]string{
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "same-origin",
} {
if got := rec.Header().Get(h); got != want {
t.Errorf("%s = %q, ожидали %q", h, got, want)
}
}
if csp := rec.Header().Get("Content-Security-Policy"); !strings.Contains(csp, "frame-ancestors 'none'") {
t.Errorf("CSP без запрета встраивания: %q", csp)
}
if cc := rec.Header().Get("Cache-Control"); !strings.Contains(cc, "no-store") {
t.Errorf("Cache-Control = %q", cc)
}
if body := rec.Body.String(); strings.Contains(body, "fonts.googleapis.com") || strings.Contains(body, "fonts.gstatic.com") {
t.Error("страница тянет шрифты со стороннего сайта")
}
}
// TestImpersonateNeedsPost — вход сотрудника в кабинет клиента не выполняется по ссылке.
func TestImpersonateNeedsPost(t *testing.T) {
bj := fakeBJ(t)
s, err := New(Config{BJURL: bj.URL, BJKey: "lk-key"})
if err != nil {
t.Fatal(err)
}
h := s.Handler()
req := httptest.NewRequest(http.MethodGet, "/impersonate?contract=BR-1", nil)
req.AddCookie(&http.Cookie{Name: "bj_lk", Value: "sess-1"})
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if loc := rec.Header().Get("Location"); loc != "/clients" {
t.Fatalf("GET /impersonate: код %d, Location %q — должен отправлять к списку клиентов без переключения", rec.Code, loc)
}
for _, c := range rec.Result().Cookies() {
if c.Name == "bj_lk" && c.Value != "" && c.Value != "sess-1" {
t.Fatal("GET /impersonate переключил сессию")
}
}
}
// TestAgentMode — режим ожидания: запрос страницы попадает в очередь, ядро забирает
// его своим подключением и возвращает ответ.
func TestAgentMode(t *testing.T) {
s, err := New(Config{BJKey: "lk-key", Agent: true})
if err != nil {
t.Fatal(err)
}
if !s.Configured() {
t.Fatal("в режиме ожидания адрес ядра не нужен")
}
h := s.Handler()
// «ядро»: подключается к кабинету, забирает запрос и отвечает
done := make(chan string, 1)
go func() {
for i := 0; i < 50; i++ {
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/agent/tasks", nil)
req.Header.Set("X-BJ-Key", "lk-key")
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
time.Sleep(10 * time.Millisecond)
continue
}
var task struct{ ID, Method, Path string }
if json.Unmarshal(rec.Body.Bytes(), &task) != nil {
continue
}
done <- task.Path
body := base64.StdEncoding.EncodeToString([]byte(`{"session":"sess-1","full_name":"Клиент","contract":"BR-1"}`))
payload := fmt.Sprintf(`{"id":%q,"status":200,"body":%q}`, task.ID, body)
rr := httptest.NewRecorder()
rq := httptest.NewRequest(http.MethodPost, "/agent/result", strings.NewReader(payload))
rq.Header.Set("X-BJ-Key", "lk-key")
h.ServeHTTP(rr, rq)
return
}
}()
csrf := csrfFromPage(t, h)
form := url.Values{"login": {"client"}, "password": {"secret"}, "csrf": {csrf.Value}}
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(csrf)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusSeeOther {
t.Fatalf("вход через очередь: код %d, тело %s", rec.Code, rec.Body.String())
}
select {
case path := <-done:
if path != "/api/lk/login" {
t.Fatalf("ядро получило %q", path)
}
default:
t.Fatal("ядро не получило запрос")
}
// чужой ключ к каналу не подпускаем
rec = httptest.NewRecorder()
bad := httptest.NewRequest(http.MethodGet, "/agent/tasks", nil)
bad.Header.Set("X-BJ-Key", "не тот ключ")
h.ServeHTTP(rec, bad)
if rec.Code != http.StatusForbidden {
t.Fatalf("чужой ключ принят: код %d", rec.Code)
}
}