package lkgateway import ( "context" "database/sql" "fmt" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m" ) func TestOneCVerdict(t *testing.T) { for _, c := range []struct { in []string want string }{ {nil, "wait"}, {[]string{"NEW"}, "wait"}, {[]string{"CONFIRMED", "NEW"}, "wait"}, {[]string{"CONFIRMED", "CONFIRMED"}, "pass"}, {[]string{"CONFIRMED", "REJECTED"}, "reject"}, {[]string{"NEW", "CANCELLED"}, "reject"}, } { if got := onecVerdict(c.in); got != c.want { t.Errorf("onecVerdict(%v) = %s, нужно %s", c.in, got, c.want) } } } func TestOneCParseIPs(t *testing.T) { got, err := onecParseIPs("10.0.0.15, 10.0.1.0/24;10.0.0.16") if err != nil || strings.Join(got, ",") != "10.0.0.15/32,10.0.1.0/24,10.0.0.16/32" { t.Fatalf("got %v, %v", got, err) } for _, bad := range []string{"10.0.0.0/16", "1c-server", "::1", "10.0.0.300"} { if _, err := onecParseIPs(bad); err == nil { t.Errorf("%q должен быть отвергнут", bad) } } } func TestOneCInstrNo(t *testing.T) { for in, want := range map[string]int64{"123456": 123456, " 77 ": 77, "1.5e3": 1500, "": 0, "0": 0, "abc": 0, "12.5": 0} { if got := onecInstrNo(in); got != want { t.Errorf("onecInstrNo(%q) = %d, нужно %d", in, got, want) } } } func TestOneCPassword(t *testing.T) { pw, err := onecPassword() if err != nil || len(pw) != onecPwLen || strings.Trim(pw, onecPwAlphabet) != "" { t.Fatalf("пароль %q, %v", pw, err) } } func TestOneCTemplates(t *testing.T) { a, err := newAdmin() if err != nil { t.Fatal(err) } now := time.Now() rows := []OneCRow{ {GUID: "g1", ISIN: "RU0009029540", InstrNo: 1001, Qty: 10, Status: "NEW", BJState: "WAIT_1C", CreatedAt: now}, {GUID: "g1", ISIN: "RU0009029557", InstrNo: 1002, Qty: 5, Status: "REJECTED", Reason: "Недостаточно бумаг", CheckedBy: "Иванова А. С.", CheckedAt: now, BJState: "WAIT_1C"}, } rec := httptest.NewRecorder() render(rec, a.claim, claimData{page: nowPage("Заявка", "claims"), Claim: ClaimView{ID: "0123456789abcdef", M2MGUID: "g1", Status: "validated"}, OneC: rows}) if rec.Code != http.StatusOK { t.Fatalf("карточка: код %d: %s", rec.Code, rec.Body.String()) } body := rec.Body.String() for _, want := range []string{"Проверка 1С", "№1001", "ждёт решения 1С", "Недостаточно бумаг", "Перевод ждёт решения 1С"} { // кнопки решения — только с правом operate if !strings.Contains(body, want) { t.Errorf("в карточке нет %q", want) } } var s Settings s.OneC.Check = OneCCheckSettings{Enabled: true, ClientIPs: "10.0.0.15/32", AccessOK: true, AccessAt: now, AccessMsg: "логин onec_bj"} rec = httptest.NewRecorder() render(rec, a.setup, SetupData{page: nowPage("Настройка", "setup"), Settings: s, OneCCheck: OneCCheckView{Waiting: 1, Rows: rows, Password: "Secret123", Host: "10.10.10.9"}}) if rec.Code != http.StatusOK { t.Fatalf("настройки: код %d: %s", rec.Code, rec.Body.String()) } body = rec.Body.String() for _, want := range []string{"Проверка поручений в 1С", "10.0.0.15/32", "Secret123", "Выдать новый пароль", "Закрыть вход", "ждёт решения 1С"} { if !strings.Contains(body, want) { t.Errorf("в настройках нет %q", want) } } } // TestOneCLive — объекты для 1С на настоящей PostgreSQL: BJ_ONEC_LIVE_DSN — база bj (владелец bj), // BJ_ONEC_LIVE_1C_DSN — та же база под логином onec_bj (вход открыт bj-onec-admin access). func TestOneCLive(t *testing.T) { dsn, dsn1c := os.Getenv("BJ_ONEC_LIVE_DSN"), os.Getenv("BJ_ONEC_LIVE_1C_DSN") if dsn == "" || dsn1c == "" { t.Skip("BJ_ONEC_LIVE_DSN / BJ_ONEC_LIVE_1C_DSN не заданы") } ctx := context.Background() rc, err := NewRuntimeConfig(t.TempDir() + "/setup.json") if err != nil { t.Fatal(err) } if err := rc.UpdatePostgres(PostgresSettings{DSN: dsn}); err != nil { t.Fatal(err) } saved := globalQuikLinks globalQuikLinks = newQuikLinkStore(t.TempDir() + "/quik.json") defer func() { globalQuikLinks = saved }() db, err := onecDB(ctx, rc) if err != nil { t.Fatal(err) } base := 990000000000 + time.Now().Unix()%1000000*10 guid := fmt.Sprintf("zz-onec-live-%d", base) defer db.ExecContext(ctx, `DELETE FROM onec.instr WHERE guid LIKE 'zz-onec-live-%'`) isins := []string{"RU000ZZTST01", "RU000ZZTST02", "RU000ZZTST03"} req := &m2m.M2MTransferRequest{} req.Header.GUID = m2m.UUID(guid) for i, isin := range isins { is, n := m2m.ISIN(isin), uint64(10*(i+1)) req.Data.TransferredSecurities.Securities = append(req.Data.TransferredSecurities.Securities, m2m.RequestSecurity{SecurityCode: m2m.SecurityCode(fmt.Sprintf("ZZ%d", i)), SecurityDetails: m2m.SecurityDetails{ISIN: &is}, Quantity: m2m.Quantity{Whole: &n}}) if i < 2 { // третья бумага — без поручения МНП (решает оператор) no := fmt.Sprint(base + int64(i)) globalQuikLinks.update(guid, isin, func(l *QuikLink) { l.Direction, l.ClientID, l.InstrNo, l.State, l.Qty = "out", "C-ZZ", no, "registered", float64(n) }) } } if err := onecUpsert(ctx, db, req); err != nil { t.Fatal(err) } if err := onecUpsert(ctx, db, req); err != nil { // повтор — без дублей t.Fatal(err) } sts, err := onecRequestStatuses(ctx, db, req) if err != nil || strings.Join(sts, ",") != "NEW,NEW,NEW" || onecVerdict(sts) != "wait" { t.Fatalf("статусы %v, %v", sts, err) } one, err := sql.Open("pgx", dsn1c) if err != nil { t.Fatal(err) } defer one.Close() var n int if err := one.QueryRowContext(ctx, `SELECT count(*) FROM onec.instr_queue WHERE status = 'NEW' AND instr_no BETWEEN $1 AND $2`, base, base+1).Scan(&n); err != nil || n != 2 { t.Fatalf("1С видит в очереди %d строк (нужно 2, строка без МНП не видна): %v", n, err) } if last := onecLastRead(ctx, db); time.Since(last) > time.Minute { t.Errorf("чтение очереди не отмечено: %v", last) } call := func(no int64, st, reason string) string { var out string if err := one.QueryRowContext(ctx, `SELECT onec.set_decision($1, $2, $3, 'тест bj', NULL)`, no, st, reason).Scan(&out); err != nil { t.Fatalf("set_decision: %v", err) } return out } for _, c := range []struct { no int64 st, reason, ok string }{ {base, "CONFIRMED", "", "OK"}, {base, "confirmed", "", "ALREADY"}, {base, "REJECTED", "передумали", "CONFLICT"}, {base + 1, "REJECTED", "", "ERROR: при отказе нужна причина"}, {base + 1, "MAYBE", "", "ERROR: статус — CONFIRMED или REJECTED"}, {base + 7, "CONFIRMED", "", "NOT_FOUND"}, {base + 1, "CONFIRMED", "", "OK"}, } { if got := call(c.no, c.st, c.reason); got != c.ok { t.Errorf("set_decision(%d, %s) = %q, нужно %q", c.no, c.st, got, c.ok) } } // 1С — только очередь и решение for _, q := range []string{`SELECT count(*) FROM onec.instr`, `SELECT count(*) FROM onec.reads`, `UPDATE onec.instr SET status = 'CONFIRMED'`, `CREATE TABLE public.zz_onec (x int)`, `SELECT count(*) FROM lk.transfer_requests`} { if _, err := one.ExecContext(ctx, q); err == nil || !strings.Contains(err.Error(), "42501") { // insufficient_privilege t.Errorf("%s под onec_bj: нужен отказ в доступе, получено %v", q, err) } } sts, _ = onecRequestStatuses(ctx, db, req) if onecVerdict(sts) != "wait" { t.Fatalf("без решения по бумаге без МНП перевод не уходит: %v", sts) } if n, err := onecOperatorDecide(ctx, rc, guid, "confirm", "", "тест"); err != nil || n != 1 { t.Fatalf("решение оператора: %d, %v", n, err) } sts, _ = onecRequestStatuses(ctx, db, req) if onecVerdict(sts) != "pass" { t.Fatalf("все подтверждены — перевод уходит: %v", sts) } rows := onecRowsForTest(ctx, t, db, guid) if len(rows) != 3 || !rows[2].ByOperator || rows[0].CheckedBy != "тест bj" { t.Fatalf("строки %+v", rows) } } func onecRowsForTest(ctx context.Context, t *testing.T, db *sql.DB, guid string) []OneCRow { rows, err := onecQuery(ctx, db, `WHERE guid = $1 ORDER BY id`, guid) if err != nil { t.Fatal(err) } return rows }