package lkgateway import ( "context" "io" "net/http" "os" "os/exec" "path/filepath" "strings" "testing" "time" ) // PHP-ретранслятор в режиме ожидания против настоящего цикла ядра. Нужен интерпретатор // PHP: путь в переменной PHP_BIN, иначе тест пропускается. func TestLKPHPRelayAgentMode(t *testing.T) { phpBin := os.Getenv("PHP_BIN") if phpBin == "" { t.Skip("PHP_BIN не задан") } const key = "lk-php-agent-key" site := filepath.Join(t.TempDir(), "site") if err := os.MkdirAll(site, 0o755); err != nil { t.Fatal(err) } src, err := os.ReadFile("../../modules/lk/relay-php/index.php") if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(site, "index.php"), src, 0o644); err != nil { t.Fatal(err) } // настройки сразу в файле — установщик проверен отдельно data := filepath.Join(filepath.Dir(site), "bj-lk-data") _ = os.MkdirAll(data, 0o700) if err := os.WriteFile(filepath.Join(data, "config.php"), []byte("\n{\"mode\":\"agent\",\"bj_key\":\""+key+"\",\"title\":\"PHP\"}"), 0o600); err != nil { t.Fatal(err) } const addr = "127.0.0.1:18097" cmd := exec.Command(phpBin, "-S", addr, "index.php") cmd.Dir = site // Встроенный сервер PHP однопоточный, а очередь требует двух одновременных запросов // (ждущий браузер и подключение ядра). На хостинге это обеспечивает php-fpm/Apache. cmd.Env = append(os.Environ(), "PHP_CLI_SERVER_WORKERS=4") if err := cmd.Start(); err != nil { t.Fatal(err) } defer func() { _ = cmd.Process.Kill(); _, _ = cmd.Process.Wait() }() time.Sleep(700 * time.Millisecond) 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, "http://"+addr 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) client := &http.Client{Timeout: 40 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }} resp, err := client.Get("http://" + addr + "/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("страница через PHP-очередь: код %d, тело %.300s", resp.StatusCode, body) } if resp.Header.Get("X-Frame-Options") != "DENY" || resp.Header.Get("Content-Security-Policy") == "" { t.Error("защитные заголовки ядра не дошли через PHP") } var csrf bool for _, c := range resp.Cookies() { if c.Name == "bj_lk_csrf" && c.HttpOnly { csrf = true } } if !csrf { t.Error("кука формы не дошла через PHP") } // без ключа очередь закрыта resp2, err := client.Get("http://" + addr + "/agent/tasks") if err != nil { t.Fatal(err) } resp2.Body.Close() if resp2.StatusCode != http.StatusForbidden { t.Errorf("/agent/tasks без ключа: код %d", resp2.StatusCode) } // состояние связи для администратора — по ключу req, _ := http.NewRequest(http.MethodGet, "http://"+addr+"/health", nil) req.Header.Set("X-BJ-Key", key) resp3, err := client.Do(req) if err != nil { t.Fatal(err) } h, _ := io.ReadAll(resp3.Body) resp3.Body.Close() if resp3.StatusCode != http.StatusOK || !strings.Contains(string(h), `"mode":"agent"`) { t.Errorf("/health по ключу: %d %s", resp3.StatusCode, h) } }