Files
2026-10-07 20:13:23 +07:00

154 lines
4.1 KiB
Go

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)
}
}