a9cb055504
Старый bj-component-install отклонял обновление целиком из-за незнакомого igate-dist — вместе со своей новой версией (рабочий сервер: установщик .06.2). installerSince: с какой версии установщик знает компонент; отложенное ставится тем же нажатием после нового установщика, а если перезапускался bj-server — само после старта (Update.Pending). Тест TestLKAgentEndToEnd: ждёт счётчик агента (гонка). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
190 lines
7.0 KiB
Go
190 lines
7.0 KiB
Go
package lkgateway
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/modules/lk"
|
|
)
|
|
|
|
// Режим ожидания целиком: настоящий кабинет (modules/lk) в режиме agent и настоящий цикл
|
|
// ядра. Кабинет ставит запрос в очередь, ядро своим исходящим подключением забирает его,
|
|
// выполняет обработчиком /api/lk/* и возвращает ответ.
|
|
func TestLKAgentEndToEnd(t *testing.T) {
|
|
const key = "lk-agent-test-key"
|
|
|
|
cabinet, err := lk.New(lk.Config{BJKey: key, Agent: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cab := httptest.NewServer(cabinet.Handler())
|
|
defer cab.Close()
|
|
|
|
rc, err := NewRuntimeConfig(t.TempDir() + "/setup.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lkCfg := rc.Snapshot().LK
|
|
lkCfg.APIKey, lkCfg.AgentMode, lkCfg.CabinetURL = key, true, cab.URL
|
|
if err := rc.UpdateLK(lkCfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mux := http.NewServeMux()
|
|
registerLKAPI(mux, rc, nil)
|
|
core := &Server{rc: rc, mux: mux}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
go core.lkAgentLoop(ctx)
|
|
|
|
// Администратор проверяет связь: кабинет спрашивает ядро через очередь.
|
|
req, _ := http.NewRequest(http.MethodGet, cab.URL+"/health", nil)
|
|
req.Header.Set("X-BJ-Key", key)
|
|
client := &http.Client{Timeout: 15 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
var out struct {
|
|
OK bool `json:"ok"`
|
|
Mode string `json:"mode"`
|
|
BJ map[string]any `json:"bj"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK || !out.OK || out.Mode != "agent" || out.BJ["service"] != "bj-server" {
|
|
t.Fatalf("ответ ядра через очередь: код %d, %+v", resp.StatusCode, out)
|
|
}
|
|
// счётчик агент увеличивает после того, как отдал ответ кабинету, — клиент может получить ответ раньше
|
|
st := lkAgentStatus(rc.Snapshot())
|
|
for i := 0; i < 40 && (!st.Connected || st.Served == 0); i++ {
|
|
time.Sleep(50 * time.Millisecond)
|
|
st = lkAgentStatus(rc.Snapshot())
|
|
}
|
|
if !st.Connected || st.Served == 0 {
|
|
t.Fatalf("состояние канала: %+v", st)
|
|
}
|
|
|
|
// Без ключа наружу — только признак жизни, без адресов и подробностей.
|
|
resp2, err := client.Get(cab.URL + "/health")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp2.Body.Close()
|
|
var pub map[string]any
|
|
_ = json.NewDecoder(resp2.Body).Decode(&pub)
|
|
if len(pub) != 1 || pub["ok"] != true {
|
|
t.Fatalf("/health без ключа раскрывает лишнее: %+v", pub)
|
|
}
|
|
}
|
|
|
|
// Ядро выполняет через канал только запросы к API кабинета — ничего другого.
|
|
func TestLKAgentRejectsForeignPath(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"id": "1", "method": "GET", "path": "/admin/setup"})
|
|
}))
|
|
defer srv.Close()
|
|
if task, err := lkAgentFetch(context.Background(), srv.Client(), srv.URL, "k"); err == nil || task != nil {
|
|
t.Fatalf("чужой путь принят: %+v", task)
|
|
}
|
|
}
|
|
|
|
// Ретранслятор: страницы собирает ядро. Прямой режим — конверт через POST /api/lk/render,
|
|
// режим ожидания — ядро забирает конверт из очереди ретранслятора. В обоих случаях браузер
|
|
// получает страницу входа с защитными заголовками и кукой, выставленными ядром.
|
|
func TestLKRelayRendersInCore(t *testing.T) {
|
|
const key = "lk-relay-test-key"
|
|
rc, err := NewRuntimeConfig(t.TempDir() + "/setup.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lkCfg := rc.Snapshot().LK
|
|
lkCfg.APIKey = key
|
|
if err := rc.UpdateLK(lkCfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mux := http.NewServeMux()
|
|
registerLKAPI(mux, rc, nil)
|
|
coreSrv := httptest.NewServer(mux)
|
|
defer coreSrv.Close()
|
|
|
|
check := func(name string, relay *lk.Server) {
|
|
t.Helper()
|
|
rs := httptest.NewServer(relay.Handler())
|
|
defer rs.Close()
|
|
client := &http.Client{Timeout: 20 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
|
|
req, _ := http.NewRequest(http.MethodGet, rs.URL+"/login", nil)
|
|
req.Header.Set("User-Agent", "Проверочный браузер")
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", name, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode != http.StatusOK || !strings.Contains(string(body), "Логин или номер договора") {
|
|
t.Fatalf("%s: код %d, тело %.200s", name, resp.StatusCode, body)
|
|
}
|
|
if resp.Header.Get("Content-Security-Policy") == "" || resp.Header.Get("X-Frame-Options") != "DENY" {
|
|
t.Errorf("%s: защитные заголовки ядра не дошли до браузера", name)
|
|
}
|
|
var csrf bool
|
|
for _, c := range resp.Cookies() {
|
|
if c.Name == "bj_lk_csrf" && c.HttpOnly {
|
|
csrf = true
|
|
}
|
|
}
|
|
if !csrf {
|
|
t.Errorf("%s: кука формы от ядра не дошла", name)
|
|
}
|
|
// API ядра через ретранслятор недостижимо: ядро считает это страницей и отвечает 404
|
|
resp2, err := client.Get(rs.URL + "/api/lk/ping")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resp2.Body.Close()
|
|
if resp2.StatusCode != http.StatusNotFound {
|
|
t.Errorf("%s: /api/lk/ping через ретранслятор: код %d, ожидали 404", name, resp2.StatusCode)
|
|
}
|
|
}
|
|
|
|
direct, err := lk.New(lk.Config{BJKey: key, BJURL: coreSrv.URL, Relay: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
check("прямой режим", direct)
|
|
|
|
agent, err := lk.New(lk.Config{BJKey: key, Agent: true, Relay: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
agentSrv := httptest.NewServer(agent.Handler())
|
|
defer agentSrv.Close()
|
|
lkCfg.AgentMode, lkCfg.CabinetURL = true, agentSrv.URL
|
|
if err := rc.UpdateLK(lkCfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
core := &Server{rc: rc, mux: mux}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
go core.lkAgentLoop(ctx)
|
|
// у режима ожидания ретранслятор уже запущен как agentSrv — проверяем через него
|
|
client := &http.Client{Timeout: 20 * time.Second}
|
|
resp, err := client.Get(agentSrv.URL + "/login")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK || !strings.Contains(string(body), "Логин или номер договора") {
|
|
t.Fatalf("режим ожидания: код %d, тело %.200s", resp.StatusCode, body)
|
|
}
|
|
}
|