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) } }