package lkgateway_test import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/lkgateway" ) func newServer(t *testing.T) *lkgateway.Server { t.Helper() srv, err := lkgateway.NewServer(lkgateway.ServerConfig{ Addr: ":0", DefaultSender: "MC0079200000", DefaultReceiver: "MC0010300000", MockDecisionDelay: 50 * time.Millisecond, CheckOptions: func() lkgateway.CheckOptions { return lkgateway.CheckOptions{Profile: "test", CryptoProvider: "stub"} }, }) if err != nil { t.Fatalf("NewServer: %v", err) } return srv } func validBody() string { return `{ "investor": { "id": "11111111-1111-1111-1111-111111111111", "last_name": "Иванов", "first_name": "Иван", "middle_name": "Иванович", "document": {"document_type": "21", "series": "4512", "number": "654321"} }, "transferring_depository_inn": "0702345678", "receiving_depository_inn": "0710987654", "cost_info": {"no": {}}, "securities": [{ "security_code": "MM0766162534", "security_details": {"isin": "RU0007661625"}, "quantity": {"whole": 1500}, "settlement_accounts": [{ "settlement_requisites_inn": "7702070139", "settlement_location": { "deponent_code": "DP789456", "account_id": "31MC0021900000F01", "section_id": "P001" } }] }], "signed_document": "dGVzdA==", "signature_format": "XMLDSig-GOST" }` } func TestCreateAndGetClaim(t *testing.T) { srv := newServer(t) mux := srv.Mux() w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/back_office/claims/", strings.NewReader(validBody())) req.Header.Set("Content-Type", "application/json") mux.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("POST claims: code=%d body=%s", w.Code, w.Body.String()) } var created lkgateway.CreateClaimResponse if err := json.Unmarshal(w.Body.Bytes(), &created); err != nil { t.Fatalf("decode: %v body=%s", err, w.Body.String()) } if created.ID == "" || !created.Success { t.Errorf("unexpected create response: %+v", created) } // GET w2 := httptest.NewRecorder() req2 := httptest.NewRequest(http.MethodGet, "/api/v1/back_office/claims/"+created.ID, nil) mux.ServeHTTP(w2, req2) if w2.Code != http.StatusOK { t.Fatalf("GET claim: code=%d body=%s", w2.Code, w2.Body.String()) } var view lkgateway.ClaimView if err := json.Unmarshal(w2.Body.Bytes(), &view); err != nil { t.Fatal(err) } if view.ID != created.ID { t.Errorf("view.ID = %s, ожидалось %s", view.ID, created.ID) } } func TestAdminHome(t *testing.T) { srv := newServer(t) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/admin/", nil) srv.Mux().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("admin home code=%d", w.Code) } body := w.Body.String() if !strings.Contains(body, "lk-gateway") { t.Errorf("в дашборде нет заголовка lk-gateway") } if !strings.Contains(body, "Состояние системы") { t.Errorf("в дашборде нет блока статуса") } } func TestAdminStatus(t *testing.T) { srv := newServer(t) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/admin/status", nil) srv.Mux().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status code=%d", w.Code) } if !strings.Contains(w.Body.String(), "postgres") { t.Errorf("в статусе нет проверки postgres") } } func TestEndToEndFlowWithMock(t *testing.T) { srv := newServer(t) // Уменьшим задержку mock для быстрого e2e. // Не достаём её напрямую — пересоздадим Server со встроенными настройками // и проверим только что после Send статус становится submitted_to_nsd → awaiting_decision. ctx, cancel := context.WithCancel(context.Background()) defer cancel() _ = ctx w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/back_office/claims/", strings.NewReader(validBody())) req.Header.Set("Content-Type", "application/json") srv.Mux().ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("POST claims: code=%d body=%s", w.Code, w.Body.String()) } var created lkgateway.CreateClaimResponse _ = json.Unmarshal(w.Body.Bytes(), &created) if created.Status != "awaiting_decision" { t.Errorf("после Submit ожидалось awaiting_decision, получено %s", created.Status) } } func TestCallbackURLSetter(t *testing.T) { srv := newServer(t) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/admin/api/callback-url?url=http://x.example/", nil) srv.Mux().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("set callback url: %d", w.Code) } } func TestListClaimsEmpty(t *testing.T) { srv := newServer(t) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/v1/back_office/claims", nil) srv.Mux().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("list claims empty: %d", w.Code) } var page lkgateway.ClaimsPage _ = json.Unmarshal(w.Body.Bytes(), &page) if len(page.Items) != 0 { t.Errorf("ожидалась пустая страница, получено %d", len(page.Items)) } } func TestInvalidJSON(t *testing.T) { srv := newServer(t) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/back_office/claims/", bytes.NewReader([]byte("not json"))) srv.Mux().ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("ожидался 400, получено %d", w.Code) } } func TestSeedClientsEndpoint(t *testing.T) { srv := newServer(t) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/admin/api/clients", nil) srv.Mux().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("clients: %d", w.Code) } var clients []map[string]any _ = json.Unmarshal(w.Body.Bytes(), &clients) if len(clients) < 5 { t.Errorf("ожидалось 5+ клиентов в seed, получено %d", len(clients)) } } func TestE2EApplyDecisionFiresCallback(t *testing.T) { // Поднимаем gateway in-process + http-эмулятор как callback-приёмник. // Дальше: POST заявки → ждём Decision из mock-канала → вручную дёргаем // ApplyDecision → проверяем что emulator получил callback. gw := newServer(t) receivedCallback := make(chan map[string]any, 1) emulator := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPatch && strings.HasPrefix(r.URL.Path, "/api/v1/back_office/claims/") { var payload map[string]any _ = json.NewDecoder(r.Body).Decode(&payload) select { case receivedCallback <- payload: default: } w.WriteHeader(http.StatusOK) return } w.WriteHeader(http.StatusOK) })) defer emulator.Close() gw.SetCallbackURL(emulator.URL) // Подаём заявку через mux (без отдельного httptest.NewServer для gateway). w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/back_office/claims/", strings.NewReader(validBody())) req.Header.Set("Content-Type", "application/json") gw.Mux().ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("POST claims: %d", w.Code) } // Mock эмитит Decision через MockDecisionDelay (50мс). Дождёмся его и // прокинем в ApplyDecision — этого делает фоновый воркер, который в // этом тесте не запущен (Run не вызывается). ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() select { case d := <-gw.Mock().Decisions(): if err := gw.Service().ApplyDecision(ctx, d); err != nil { t.Fatalf("ApplyDecision: %v", err) } case <-ctx.Done(): t.Fatal("Decision из mock не пришёл") } select { case cb := <-receivedCallback: status, _ := cb["new_status"].(string) if status != "confirmed" { t.Errorf("ожидался callback со статусом confirmed, получено %s", status) } case <-time.After(2 * time.Second): t.Fatal("callback в эмулятор не пришёл") } }