689ba36e92
The WrapUDPConn constructor returns nil when either arg is nil, so c.stats is always non-nil in all udpConn methods. Remove 8 redundant nil guards that were inconsistent with packetConn (which already skipped them). Add 65 unit tests across plugin, stats, and wrapper packages covering nil safety, race conditions, double-close, event marshaling, and reset-traffic semantics. Replace magic numbers in tests with named Kind constants.
245 lines
6.8 KiB
Go
245 lines
6.8 KiB
Go
package observer
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-gost/core/observer"
|
|
"github.com/go-gost/x/internal/plugin"
|
|
xstats "github.com/go-gost/x/observer/stats"
|
|
"github.com/go-gost/x/service"
|
|
)
|
|
|
|
func TestNewHTTPPlugin(t *testing.T) {
|
|
p := NewHTTPPlugin("test", "localhost:8080")
|
|
if p == nil {
|
|
t.Fatal("NewHTTPPlugin should not return nil")
|
|
}
|
|
hp, ok := p.(*httpPlugin)
|
|
if !ok {
|
|
t.Fatalf("NewHTTPPlugin returned %T, want *httpPlugin", p)
|
|
}
|
|
if hp.url != "http://localhost:8080" {
|
|
t.Errorf("url = %s, want http://localhost:8080", hp.url)
|
|
}
|
|
if hp.client == nil {
|
|
t.Error("client should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestNewHTTPPlugin_NoScheme(t *testing.T) {
|
|
p := NewHTTPPlugin("test", "example.com:9999")
|
|
hp := p.(*httpPlugin)
|
|
if !strings.HasPrefix(hp.url, "http://") {
|
|
t.Errorf("expected http:// prefix, got %s", hp.url)
|
|
}
|
|
}
|
|
|
|
func TestNewHTTPPlugin_KeepsHTTPSScheme(t *testing.T) {
|
|
p := NewHTTPPlugin("test", "https://example.com/api")
|
|
hp := p.(*httpPlugin)
|
|
if !strings.HasPrefix(hp.url, "https://") {
|
|
t.Errorf("expected https:// prefix, got %s", hp.url)
|
|
}
|
|
}
|
|
|
|
func TestHTTPPlugin_Observe_StatusEvent(t *testing.T) {
|
|
var receivedBody json.RawMessage
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
t.Errorf("expected POST, got %s", r.Method)
|
|
}
|
|
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
|
|
t.Errorf("Content-Type = %s, want application/json", ct)
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&receivedBody)
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"ok": true}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewHTTPPlugin("test", srv.URL)
|
|
err := p.Observe(context.Background(), []observer.Event{
|
|
service.ServiceEvent{
|
|
Kind: "service",
|
|
Service: "test-svc",
|
|
State: "running",
|
|
Msg: "started",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify the event was sent correctly
|
|
var req observeRequest
|
|
if err := json.Unmarshal(receivedBody, &req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(req.Events) != 1 {
|
|
t.Fatalf("expected 1 event, got %d", len(req.Events))
|
|
}
|
|
if req.Events[0].Kind != "service" {
|
|
t.Errorf("event kind = %s, want service", req.Events[0].Kind)
|
|
}
|
|
if req.Events[0].Service != "test-svc" {
|
|
t.Errorf("event service = %s, want test-svc", req.Events[0].Service)
|
|
}
|
|
if req.Events[0].Status == nil {
|
|
t.Fatal("status event should have Status field")
|
|
}
|
|
if req.Events[0].Status.State != "running" {
|
|
t.Errorf("state = %s, want running", req.Events[0].Status.State)
|
|
}
|
|
}
|
|
|
|
func TestHTTPPlugin_Observe_StatsEvent(t *testing.T) {
|
|
var receivedBody json.RawMessage
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&receivedBody)
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"ok": true}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewHTTPPlugin("test", srv.URL)
|
|
err := p.Observe(context.Background(), []observer.Event{
|
|
xstats.StatsEvent{
|
|
Kind: "traffic",
|
|
Service: "svc",
|
|
Client: "c1",
|
|
TotalConns: 100,
|
|
CurrentConns: 5,
|
|
InputBytes: 4096,
|
|
OutputBytes: 8192,
|
|
TotalErrs: 2,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var req observeRequest
|
|
if err := json.Unmarshal(receivedBody, &req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(req.Events) != 1 {
|
|
t.Fatalf("expected 1 event, got %d", len(req.Events))
|
|
}
|
|
if req.Events[0].Stats == nil {
|
|
t.Fatal("stats event should have Stats field")
|
|
}
|
|
if req.Events[0].Stats.TotalConns != 100 {
|
|
t.Errorf("totalConns = %d, want 100", req.Events[0].Stats.TotalConns)
|
|
}
|
|
if req.Events[0].Stats.CurrentConns != 5 {
|
|
t.Errorf("currentConns = %d, want 5", req.Events[0].Stats.CurrentConns)
|
|
}
|
|
if req.Events[0].Stats.InputBytes != 4096 {
|
|
t.Errorf("inputBytes = %d, want 4096", req.Events[0].Stats.InputBytes)
|
|
}
|
|
if req.Events[0].Stats.OutputBytes != 8192 {
|
|
t.Errorf("outputBytes = %d, want 8192", req.Events[0].Stats.OutputBytes)
|
|
}
|
|
if req.Events[0].Stats.TotalErrs != 2 {
|
|
t.Errorf("totalErrs = %d, want 2", req.Events[0].Stats.TotalErrs)
|
|
}
|
|
if req.Events[0].Client != "c1" {
|
|
t.Errorf("client = %s, want c1", req.Events[0].Client)
|
|
}
|
|
}
|
|
|
|
func TestHTTPPlugin_Observe_EmptyEvents(t *testing.T) {
|
|
p := NewHTTPPlugin("test", "http://localhost:9999")
|
|
err := p.Observe(context.Background(), nil)
|
|
if err != nil {
|
|
t.Errorf("empty events should return nil, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHTTPPlugin_Observe_ServerError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewHTTPPlugin("test", srv.URL)
|
|
err := p.Observe(context.Background(), []observer.Event{
|
|
service.ServiceEvent{Kind: "service", Service: "test"},
|
|
})
|
|
if err == nil {
|
|
t.Error("Observe should return error on non-200 response")
|
|
}
|
|
}
|
|
|
|
func TestHTTPPlugin_Observe_NotOk(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"ok": false}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewHTTPPlugin("test", srv.URL)
|
|
err := p.Observe(context.Background(), []observer.Event{
|
|
service.ServiceEvent{Kind: "service", Service: "test"},
|
|
})
|
|
if err == nil {
|
|
t.Error("Observe should return error when ok is false")
|
|
}
|
|
}
|
|
|
|
func TestHTTPPlugin_Observe_InvalidJSON(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`not json`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewHTTPPlugin("test", srv.URL)
|
|
err := p.Observe(context.Background(), []observer.Event{
|
|
service.ServiceEvent{Kind: "service", Service: "test"},
|
|
})
|
|
if err == nil {
|
|
t.Error("Observe should return error on invalid JSON response")
|
|
}
|
|
}
|
|
|
|
func TestHTTPPlugin_CustomHeader(t *testing.T) {
|
|
var receivedHeader string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
receivedHeader = r.Header.Get("X-Custom")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"ok": true}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewHTTPPlugin("test", srv.URL, plugin.HeaderOption(http.Header{
|
|
"X-Custom": []string{"my-value"},
|
|
}))
|
|
err := p.Observe(context.Background(), []observer.Event{
|
|
service.ServiceEvent{Kind: "service", Service: "test"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if receivedHeader != "my-value" {
|
|
t.Errorf("X-Custom header = %s, want my-value", receivedHeader)
|
|
}
|
|
}
|
|
|
|
func TestHTTPPlugin_Observe_WithClient(t *testing.T) {
|
|
// Ensure p.client is properly set by constructor and Observe doesn't
|
|
// hit the dead nil check.
|
|
p := NewHTTPPlugin("test", "http://example.com")
|
|
if p.(*httpPlugin).client == nil {
|
|
t.Fatal("client should not be nil after construction")
|
|
}
|
|
}
|
|
|
|
// Test that httpPlugin satisfies observer.Observer
|
|
var _ observer.Observer = (*httpPlugin)(nil)
|