Личный кабинет — модуль modules/lk: сборка в релизе, unit в комплекте, ключ вне unit-файла
Код bj-lk перенесён в modules/lk (cmd/bj-lk — запуск, флаг -version); релиз собирает bj-lk со своей версией (LK_VERSION), unit bj-lk.service встроен в комплект установщика и берёт адрес bj-server и ключ доступа из /etc/bj/lk.env, а не из unit-файла. Исправлено: признаки «администратор» и «кабинет клиента» читались из общей переменной последнего запроса (curReq) — при одновременной работе признак мог достаться чужому запросу. Теперь из кук своего запроса. Без BJ_URL/BJ_KEY кабинет показывает подсказку настройки вместо падения службы. Тесты: вход, портфель, health, флаг админа по запросу, без настройки. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package lk
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 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", "positions": []any{}})
|
||||
}
|
||||
})
|
||||
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})
|
||||
}
|
||||
})
|
||||
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()
|
||||
|
||||
form := url.Values{"login": {"client"}, "password": {"secret"}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
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("признак администратора перешёл из чужого запроса")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotConfigured(t *testing.T) {
|
||||
s, err := New(Config{BJURL: "http://127.0.0.1:8080"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s.Configured() {
|
||||
t.Fatal("без ключа кабинет не настроен")
|
||||
}
|
||||
for _, p := range []string{"/", "/health"} {
|
||||
rec := httptest.NewRecorder()
|
||||
s.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, p, nil))
|
||||
if rec.Code != http.StatusServiceUnavailable || !strings.Contains(rec.Body.String(), "BJ_KEY") {
|
||||
t.Fatalf("%s: код %d: %s", p, rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user