Initial commit: omnichannel-mcp — MCP-сервер управления платформой Omnichannel
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user