Files

245 lines
11 KiB
Go
Raw Permalink 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/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", "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})
}
})
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())
}
}
}
// Страницы кабинета в оформлении «Табло»: обзор, реестр, поручения, профиль и бланк поручения.
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{"Стоимость бумаг", "Новое поручение", "Сбербанк, ао", "Отправлено в НРД", "Поручений в работе"} {
if !strings.Contains(board, want) {
t.Errorf("на табло нет %q", want)
}
}
if !strings.Contains(board, "110 268 ₽") {
t.Errorf("на табло нет оценки портфеля")
}
reg := get("/portfolio")
for _, want := range []string{"ВТНКЦ00000001", "иностранный выпуск", "депозитарный учёт", "перевести"} {
if !strings.Contains(reg, want) {
t.Errorf("в реестре нет %q", want)
}
}
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{"ПАО Сбербанк", "Код CFI", "275,67", "дивиденды", "Перевести эту бумагу"} {
if !strings.Contains(card, want) {
t.Errorf("в карточке бумаги нет %q", want)
}
}
ops := get("/operations")
for _, want := range []string{"Операции по счетам депо", "зачисление", "Ликвидность, пай", "Отчёт клиринговой организации"} {
if !strings.Contains(ops, want) {
t.Errorf("в операциях нет %q", want)
}
}
inc := get("/income")
for _, want := range []string{"Выплачено дохода", "494,60 ₽", "Удержан налог", "дивиденды"} {
if !strings.Contains(inc, 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"))
}
}
}