package configserver import ( "context" "encoding/json" "net/http" "net/http/httptest" "sync" "testing" ) // mockState — разделяемое состояние тестового config_server. type mockState struct { mu sync.Mutex logins int failOnceApps bool flakyCalls int mutationCalls int } func newTestServer(t *testing.T) (*httptest.Server, *mockState) { t.Helper() st := &mockState{} mux := http.NewServeMux() writeJSON := func(w http.ResponseWriter, code int, body string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) _, _ = w.Write([]byte(body)) } mux.HandleFunc("/api/login", func(w http.ResponseWriter, r *http.Request) { var body map[string]string _ = json.NewDecoder(r.Body).Decode(&body) if body["username"] != "u" || body["password"] != "p" { writeJSON(w, http.StatusUnauthorized, `{"error":"Invalid username or password"}`) return } st.mu.Lock() st.logins++ st.mu.Unlock() http.SetCookie(w, &http.Cookie{Name: "sess", Value: "ok", Path: "/"}) writeJSON(w, http.StatusOK, `{"status":"ok","username":"u"}`) }) mux.HandleFunc("/api/applications", func(w http.ResponseWriter, r *http.Request) { if c, err := r.Cookie("sess"); err != nil || c.Value != "ok" { writeJSON(w, http.StatusUnauthorized, `{"error":"Authentication required"}`) return } st.mu.Lock() fail := st.failOnceApps st.failOnceApps = false st.mu.Unlock() if fail { writeJSON(w, http.StatusUnauthorized, `{"error":"Authentication required"}`) return } writeJSON(w, http.StatusOK, `[{"id":1,"name":"x","status":"OK"}]`) }) mux.HandleFunc("/api/flaky", func(w http.ResponseWriter, r *http.Request) { st.mu.Lock() st.flakyCalls++ n := st.flakyCalls st.mu.Unlock() if n == 1 { writeJSON(w, http.StatusInternalServerError, `{"error":"boom"}`) return } writeJSON(w, http.StatusOK, `{"ok":true}`) }) mux.HandleFunc("/api/mutate", func(w http.ResponseWriter, r *http.Request) { st.mu.Lock() st.mutationCalls++ st.mu.Unlock() writeJSON(w, http.StatusInternalServerError, `{"error":"boom"}`) }) ts := httptest.NewServer(mux) t.Cleanup(ts.Close) return ts, st } func newTestSession(t *testing.T, url string) *Session { t.Helper() s, err := NewSession(&Server{Alias: "t", BaseURL: url, Username: "u", Password: "p"}, false) if err != nil { t.Fatalf("NewSession: %v", err) } return s } func TestSessionLoginAndGet(t *testing.T) { ts, st := newTestServer(t) s := newTestSession(t, ts.URL) var apps []Application if err := s.getJSON(context.Background(), "/api/applications", nil, &apps); err != nil { t.Fatalf("getJSON: %v", err) } if len(apps) != 1 || apps[0].Name != "x" { t.Fatalf("неожиданный ответ: %+v", apps) } if st.logins != 1 { t.Fatalf("logins = %d, ожидалось 1", st.logins) } } func TestSessionReloginOn401(t *testing.T) { ts, st := newTestServer(t) s := newTestSession(t, ts.URL) st.failOnceApps = true var apps []Application if err := s.getJSON(context.Background(), "/api/applications", nil, &apps); err != nil { t.Fatalf("getJSON после relogin: %v", err) } if st.logins != 2 { t.Fatalf("logins = %d, ожидалось 2 (релогin при 401)", st.logins) } } func TestGetRetriedOn5xx(t *testing.T) { ts, st := newTestServer(t) s := newTestSession(t, ts.URL) var out map[string]any if err := s.getJSON(context.Background(), "/api/flaky", nil, &out); err != nil { t.Fatalf("getJSON flaky: %v", err) } if st.flakyCalls != 2 { t.Fatalf("GET-повтор не сработал: calls = %d", st.flakyCalls) } } func TestMutationNotRetried(t *testing.T) { ts, st := newTestServer(t) s := newTestSession(t, ts.URL) var out map[string]any err := s.postJSON(context.Background(), "/api/mutate", map[string]string{"x": "y"}, &out) if err == nil { t.Fatal("ожидалась ошибка 500") } if st.mutationCalls != 1 { t.Fatalf("мутация НЕ должна повторяться: calls = %d", st.mutationCalls) } if !IsDomainError(err) { t.Fatalf("ошибка 500 должна классифицироваться как доменная: %v", err) } }