154 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|