ba1aae705d
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
270 lines
11 KiB
Go
270 lines
11 KiB
Go
package lkgateway
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"net/url"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func TestAccPrefix(t *testing.T) {
|
||
for in, want := range map[string]string{"ВТНКЦ": "ВТНКЦ", " БР ": "БР", "AB_1": "AB_1", "a'b": "", "x%": "", "": ""} {
|
||
if got := cleanAccPrefix(in); got != want {
|
||
t.Errorf("cleanAccPrefix(%q)=%q, want %q", in, got, want)
|
||
}
|
||
}
|
||
if q := accSQL("LIKE N'{ACC}%'"); !strings.Contains(q, "N'ВТНКЦ%'") {
|
||
t.Errorf("accSQL по умолчанию: %s", q)
|
||
}
|
||
}
|
||
|
||
func TestDoneNSDUnknownProfile(t *testing.T) {
|
||
var s Settings
|
||
s.NSD.IGWBaseURL, s.NSD.Profile = "http://localhost:8090", "prod"
|
||
if doneNSD(s) {
|
||
t.Fatal("профиль prod неизвестен bj — шаг не должен считаться выполненным")
|
||
}
|
||
s.NSD.Profile = "prod-gost"
|
||
if !doneNSD(s) {
|
||
t.Fatal("prod-gost — известный профиль")
|
||
}
|
||
}
|
||
|
||
func TestStoreCertKindAndExpiry(t *testing.T) {
|
||
bundle := map[string]bool{}
|
||
own := NSDStoreCert{Kind: "x509", Subject: "O=Предприятие 1046,CN=Сотрудник 1046,INN=078000001046", SubjectCN: "Сотрудник 1046", IssuerCN: "Тестовый УЦ", Serial: "AA01"}
|
||
nsd := NSDStoreCert{Kind: "x509", Subject: "O=НКО АО НРД,CN=Петров,INN=007702165310", SubjectCN: "Петров", IssuerCN: "Тестовый УЦ", Serial: "BB02"}
|
||
ca := NSDStoreCert{Kind: "x509", SubjectCN: "Тестовый УЦ", SelfSigned: true}
|
||
for c, want := range map[*NSDStoreCert]string{&own: "own", &nsd: "counterpart", &ca: "ca"} {
|
||
if got := storeCertKind(*c, bundle); got != want {
|
||
t.Errorf("%s: %s, want %s", c.SubjectCN, got, want)
|
||
}
|
||
}
|
||
if subjectINN(nsd.Subject) != "7702165310" {
|
||
t.Errorf("subjectINN: %q", subjectINN(nsd.Subject))
|
||
}
|
||
|
||
// уведомления: за 20 дней — одно (отметка 30), повторно не шлётся; за 5 дней — следующее (7)
|
||
dir := t.TempDir()
|
||
certExpiry.file = filepath.Join(dir, "cert-expiry.json")
|
||
rc, err := NewRuntimeConfig(filepath.Join(dir, "setup.json"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
own.NotAfter = time.Now().AddDate(0, 0, 20).Format("02.01.2006")
|
||
st := rc.Snapshot().NSDCerts
|
||
st.LastStore = []NSDStoreCert{own, nsd, ca}
|
||
_ = rc.UpdateNSDCerts(st)
|
||
count := func() int { return len(rc.Snapshot().News.Items) }
|
||
certExpiryTick(rc)
|
||
n1 := count()
|
||
certExpiryTick(rc)
|
||
if n1 != 1 || count() != 1 {
|
||
t.Fatalf("за 20 дней: ожидалось одно уведомление, было %d, затем %d", n1, count())
|
||
}
|
||
own.NotAfter = time.Now().AddDate(0, 0, 5).Format("02.01.2006")
|
||
st.LastStore = []NSDStoreCert{own}
|
||
_ = rc.UpdateNSDCerts(st)
|
||
certExpiryTick(rc)
|
||
if count() != 2 {
|
||
t.Fatalf("за 5 дней: ожидалось второе уведомление, всего %d", count())
|
||
}
|
||
raw, _ := os.ReadFile(certExpiry.file)
|
||
if !strings.Contains(string(raw), "7") {
|
||
t.Fatalf("отметка 7 не сохранена: %s", raw)
|
||
}
|
||
}
|
||
|
||
func TestSafeReturnPathKeepsStep(t *testing.T) {
|
||
for in, want := range map[string]string{
|
||
"/admin/wizard?step=4": "/admin/wizard?step=4",
|
||
"/admin/wizard?step=3&sub=done&flash=x": "/admin/wizard?step=3&sub=done",
|
||
"/admin/setup#sec-org": "/admin/setup",
|
||
"/admin/wizard?step=4%0d%0aX": "/admin/wizard",
|
||
"//evil.example/admin": "",
|
||
"/admin/x?a=<script>": "/admin/x",
|
||
} {
|
||
if got := safeReturnPath(in); got != want {
|
||
t.Errorf("safeReturnPath(%q)=%q, want %q", in, got, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestFirstRunRestoreNeedsCode(t *testing.T) {
|
||
if !authExempt("/first-run/backup/list") || authExempt("/first-runx") {
|
||
t.Fatal("пути восстановления при первичной настройке должны быть без входа, но не шире")
|
||
}
|
||
mux := http.NewServeMux()
|
||
registerFirstRunRestore(mux)
|
||
rec := httptest.NewRecorder()
|
||
mux.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/first-run/backup/restore", strings.NewReader("code=1234&name=2026-10-05_1445")))
|
||
if !strings.Contains(rec.Body.String(), `"ok":false`) {
|
||
t.Fatalf("без ожидающей первичной настройки восстановление не должно запускаться: %s", rec.Body.String())
|
||
}
|
||
}
|
||
|
||
func TestHTTPSSelfSignedAndRedirect(t *testing.T) {
|
||
old := tlsDir
|
||
tlsDir = t.TempDir()
|
||
defer func() { tlsDir = old }()
|
||
if err := ensureSelfSigned(false); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if cn, until, err := tlsCertInfo(); err != nil || cn == "" || certDaysLeft(until) < 1000 {
|
||
t.Fatalf("сертификат: %q %v %v", cn, until, err)
|
||
}
|
||
if _, err := loadTLSCert(nil); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
rc, _ := NewRuntimeConfig(t.TempDir() + "/setup.json")
|
||
_ = rc.UpdateHTTPS(HTTPSSettings{Redirect: true})
|
||
h := httpsRedirect(rc, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) }))
|
||
cases := []struct {
|
||
path, remote string
|
||
want int
|
||
}{
|
||
{"/admin/setup", "10.10.10.5:5000", http.StatusFound},
|
||
{"/api/v1/lk/x", "10.10.10.5:5000", http.StatusTeapot},
|
||
{"/healthz", "10.10.10.5:5000", http.StatusTeapot},
|
||
{"/admin/setup", "127.0.0.1:5000", http.StatusTeapot},
|
||
}
|
||
for _, c := range cases {
|
||
req := httptest.NewRequest(http.MethodGet, "http://10.10.10.6:8080"+c.path, nil)
|
||
req.RemoteAddr = c.remote
|
||
rec := httptest.NewRecorder()
|
||
h.ServeHTTP(rec, req)
|
||
if rec.Code != c.want {
|
||
t.Errorf("%s с %s: %d, ожидалось %d", c.path, c.remote, rec.Code, c.want)
|
||
}
|
||
if c.want == http.StatusFound && rec.Header().Get("Location") != "https://10.10.10.6:8443/admin/setup" {
|
||
t.Errorf("перенаправление: %s", rec.Header().Get("Location"))
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestReceiverUnknownRejected(t *testing.T) {
|
||
if !handbookLoaded() {
|
||
t.Skip("справочник участников не вшит")
|
||
}
|
||
svc := &Service{defaultSender: "MC0413600000", defaultReceiver: "MC0010300000"}
|
||
if _, err := svc.BuildRequest(context.Background(), CreateClaimRequest{ReceivingDepositoryINN: "0000000001"}); err == nil || !errors.Is(err, errReceiverUnknown) {
|
||
t.Fatalf("неизвестный депозитарий получателя должен давать errReceiverUnknown, а не чужой код: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestBuildRequestNeedsOrganization(t *testing.T) {
|
||
svc := &Service{}
|
||
if _, err := svc.BuildRequest(context.Background(), CreateClaimRequest{}); err == nil || !strings.Contains(err.Error(), "организация не задана") {
|
||
t.Fatalf("без кода отправителя перевод не должен формироваться: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestKeyArchiveContour(t *testing.T) {
|
||
dir := t.TempDir()
|
||
raw, err := nsdCertsFS.ReadFile("nsd-certs/nsd-test-gost-petrov-20240924.cer")
|
||
if err != nil {
|
||
t.Skip(err)
|
||
}
|
||
_ = os.MkdirAll(filepath.Join(dir, "spr1"), 0o755)
|
||
_ = os.WriteFile(filepath.Join(dir, "spr1", "user.cer"), raw, 0o644)
|
||
if c, iss := keyArchiveContour(dir); c != "test3" || iss == "" {
|
||
t.Fatalf("тестовый сертификат: %q %q", c, iss)
|
||
}
|
||
raw, _ = nsdCertsFS.ReadFile("nsd-certs/nsd-prod-gost-shlyappo-20260821.cer")
|
||
dir2 := t.TempDir()
|
||
_ = os.WriteFile(filepath.Join(dir2, "user.cer"), raw, 0o644)
|
||
if c, _ := keyArchiveContour(dir2); c != "prod" {
|
||
t.Fatalf("боевой сертификат: %q", c)
|
||
}
|
||
}
|
||
|
||
func TestWizardChaptersWithoutLicense(t *testing.T) {
|
||
rc, _ := NewRuntimeConfig(t.TempDir() + "/setup.json")
|
||
done := map[int]bool{2: true, 6: true}
|
||
path, chapters := wizPath(rc, done, 4)
|
||
want := []int{1, 12, 4, 7, 8, 9, 10} // без лицензии нет учёта DEPO, QUIK и модулей; база и Валидата — сделаны
|
||
if fmt.Sprint(path) != fmt.Sprint(want) {
|
||
t.Fatalf("путь %v, ожидался %v", path, want)
|
||
}
|
||
if len(chapters) != 4 || !chapters[1].Current || chapters[1].Steps[0].N != 4 {
|
||
t.Fatalf("главы: %+v", chapters)
|
||
}
|
||
if p, n := wizNeighbours(path, 4); p != 12 || n != 7 {
|
||
t.Fatalf("соседи шага 4: %d %d", p, n)
|
||
}
|
||
if d := wizDefaultStep(path, map[int]bool{1: true, 4: false}); d != 4 {
|
||
t.Fatalf("по умолчанию: %d", d)
|
||
}
|
||
all := map[int]bool{}
|
||
for _, n := range path {
|
||
all[n] = true
|
||
}
|
||
if d := wizDefaultStep(path, all); d != 10 {
|
||
t.Fatalf("всё сделано — проверка, а не %d", d)
|
||
}
|
||
if len(wizModuleViews(rc, rc.Snapshot())) != 0 {
|
||
t.Fatal("без лицензии модулей в мастере быть не должно")
|
||
}
|
||
}
|
||
|
||
func TestLettersFill(t *testing.T) {
|
||
var s Settings
|
||
s.NSD.OrgName, s.NSD.OrgINN, s.NSD.OrgCode = "АО «Тест»", "7700000001", "MC0000000001"
|
||
s.NSD.DeponentCode, s.NSD.AccountID, s.NSD.SectionID = "MC0000000001", "HL000000001C", "36MC0000000001F00"
|
||
s.Fancy.Server = "sql.example"
|
||
for _, k := range []string{"nsd-test", "nsd-prod", "arqa", "dba"} {
|
||
l, ok := letterFor(s, k)
|
||
if !ok || !strings.Contains(l.Body, "АО «Тест»") || l.Subject == "" {
|
||
t.Errorf("%s: %+v", k, l)
|
||
}
|
||
}
|
||
l, _ := letterFor(s, "nsd-test")
|
||
if !strings.Contains(l.Body, "HL000000001C") || !strings.Contains(l.Body, "ГОСТ") {
|
||
t.Error("в письме НРД нет реквизитов")
|
||
}
|
||
s.Fancy.DBType = "postgres"
|
||
if l, _ := letterFor(s, "dba"); strings.Contains(l.Body, "SQL Server Agent") || !strings.Contains(l.Body, "postgres") {
|
||
t.Error("письмо администратору PostgreSQL не должно говорить о SQL Agent")
|
||
}
|
||
if _, ok := letterFor(s, "nope"); ok {
|
||
t.Error("неизвестное письмо")
|
||
}
|
||
}
|
||
|
||
func TestRuPlural(t *testing.T) {
|
||
for n, want := range map[int]string{1: "участник", 2: "участника", 4: "участника", 5: "участников", 11: "участников", 12: "участников", 21: "участник", 162: "участника", 111: "участников", 0: "участников"} {
|
||
if got := ruPlural(n, "участник", "участника", "участников"); got != want {
|
||
t.Errorf("%d: %q, ожидалось %q", n, got, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Без лицензии QUIK МНП маршрута «ошибки QUIK» в форме нет, а сохранение формы не стирает его прежнее значение.
|
||
func TestNotifyKindsByLicense(t *testing.T) {
|
||
rc, _ := NewRuntimeConfig(t.TempDir() + "/setup.json")
|
||
for _, k := range visibleNotifyKinds(rc) {
|
||
if k.Key == "quik" {
|
||
t.Fatal("без лицензии QUIK МНП маршрута QUIK быть не должно")
|
||
}
|
||
}
|
||
if len(visibleNotifyKinds(rc)) != len(notifyKinds)-1 {
|
||
t.Fatalf("типы: %+v", visibleNotifyKinds(rc))
|
||
}
|
||
h := &setupHandlers{rc: rc}
|
||
form := url.Values{"enabled": {"on"}, "emails": {"depo@example.ru"}, "route_incoming_depo": {"1"}}
|
||
req := httptest.NewRequest(http.MethodPost, "/admin/setup/incoming-notify", strings.NewReader(form.Encode()))
|
||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||
h.saveIncomingNotify(httptest.NewRecorder(), req)
|
||
r := rc.Snapshot().Incoming.Routes
|
||
if r["incoming"] != "depo" || r["quik"] != defaultNotifyRoutes["quik"] {
|
||
t.Fatalf("маршруты после сохранения: %+v", r)
|
||
}
|
||
}
|