Ctrl K
ComunidadGuías…

streaming_test.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package aastro

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"

	"github.com/voidrunner3074/aastro/sdk"
)

func TestStreaming_SSEResponse(t *testing.T) {
	t.Run("forwards body and headers without buffering", func(t *testing.T) {
		u := &mockProxyUpstream{
			upstreamName: "sse",
			proxyFn: func(w http.ResponseWriter, _ *http.Request) error {
				w.Header().Set("Content-Type", "text/event-stream")
				w.Header().Set("Cache-Control", "no-cache")
				w.WriteHeader(http.StatusOK)
				_, _ = fmt.Fprint(w, "data: hello\n\n")
				return nil
			},
		}

		r := newTestRouter([]flow{streamingFlow("/stream", u)}, nil, nil)
		req := httptest.NewRequest(http.MethodGet, "/stream", nil)
		rec := httptest.NewRecorder()
		r.ServeHTTP(rec, req)

		res := rec.Result()
		defer res.Body.Close()
		body, _ := io.ReadAll(res.Body)

		assert.Equal(t, http.StatusOK, res.StatusCode)
		assert.Equal(t, "text/event-stream", res.Header.Get("Content-Type"))
		assert.Equal(t, "no-cache", res.Header.Get("Cache-Control"))
		assert.Empty(t, res.Header.Get("Content-Length"))
		assert.Equal(t, "data: hello\n\n", string(body))
	})
}

func TestStreaming_MultipleSSEEvents(t *testing.T) {
	t.Run("forwards all events in order", func(t *testing.T) {
		events := "data: first\n\ndata: second\n\ndata: third\n\n"

		u := &mockProxyUpstream{
			upstreamName: "sse-multi",
			proxyFn: func(w http.ResponseWriter, _ *http.Request) error {
				w.Header().Set("Content-Type", "text/event-stream")
				w.WriteHeader(http.StatusOK)
				_, _ = fmt.Fprint(w, events)
				return nil
			},
		}

		r := newTestRouter([]flow{streamingFlow("/events", u)}, nil, nil)
		req := httptest.NewRequest(http.MethodGet, "/events", nil)
		rec := httptest.NewRecorder()
		r.ServeHTTP(rec, req)

		res := rec.Result()
		defer res.Body.Close()
		body, _ := io.ReadAll(res.Body)

		assert.Equal(t, http.StatusOK, res.StatusCode)
		assert.Equal(t, events, string(body))
		assert.Equal(t, 3, strings.Count(string(body), "data:"))
	})
}

func TestStreaming_RequestPlugin(t *testing.T) {
	t.Run("runs the plugin before forwarding to upstream", func(t *testing.T) {
		var pluginSaw string

		plugin := &mockPlugin{
			name: "auth",
			typ:  sdk.PluginTypeRequest,
			fn: func(ctx sdk.Context) {
				ctx.Request().Header.Set("X-Auth", "injected")
			},
		}

		u := &mockProxyUpstream{
			upstreamName: "backend",
			proxyFn: func(w http.ResponseWriter, req *http.Request) error {
				pluginSaw = req.Header.Get("X-Auth")
				w.WriteHeader(http.StatusOK)
				return nil
			},
		}

		r := newTestRouter([]flow{streamingFlow("/plugin", u, plugin)}, nil, nil)
		req := httptest.NewRequest(http.MethodGet, "/plugin", nil)
		rec := httptest.NewRecorder()
		r.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusOK, rec.Code)
		assert.Equal(t, "injected", pluginSaw)
	})
}

func TestStreaming_UpstreamErrorsBeforeAnyWrite(t *testing.T) {
	t.Run("returns 502", func(t *testing.T) {
		u := &mockProxyUpstream{
			upstreamName: "broken",
			proxyFn: func(_ http.ResponseWriter, _ *http.Request) error {
				return errors.New("upstream connection refused")
			},
		}

		r := newTestRouter([]flow{streamingFlow("/broken", u)}, nil, nil)
		req := httptest.NewRequest(http.MethodGet, "/broken", nil)
		rec := httptest.NewRecorder()
		r.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusBadGateway, rec.Code)
	})
}

func TestStreaming_UpstreamErrorsAfterPartialWrite(t *testing.T) {
	t.Run("does not panic and preserves the original status", func(t *testing.T) {
		u := &mockProxyUpstream{
			upstreamName: "partial",
			proxyFn: func(w http.ResponseWriter, _ *http.Request) error {
				w.WriteHeader(http.StatusOK)
				_, _ = fmt.Fprint(w, "data: partial\n\n")
				return errors.New("connection reset by peer")
			},
		}

		r := newTestRouter([]flow{streamingFlow("/partial", u)}, nil, nil)
		req := httptest.NewRequest(http.MethodGet, "/partial", nil)
		rec := httptest.NewRecorder()

		assert.NotPanics(t, func() { r.ServeHTTP(rec, req) })
		assert.Equal(t, http.StatusOK, rec.Code)
	})
}

func TestStreaming_NonProxyCapableUpstream(t *testing.T) {
	t.Run("returns 500", func(t *testing.T) {
		u := &mockUpstream{upstreamName: "plain"}

		r := newTestRouter([]flow{{
			path:      "/noproxy",
			method:    http.MethodGet,
			streaming: true,
			upstreams: []upstream{u},
		}}, nil, nil)

		req := httptest.NewRequest(http.MethodGet, "/noproxy", nil)
		rec := httptest.NewRecorder()
		r.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusInternalServerError, rec.Code)
	})
}

func TestStreaming_MultipleUpstreams(t *testing.T) {
	t.Run("returns 500 because streaming requires exactly one", func(t *testing.T) {
		makeU := func(n string) upstream {
			return &mockProxyUpstream{
				upstreamName: n,
				proxyFn: func(w http.ResponseWriter, _ *http.Request) error {
					w.WriteHeader(http.StatusOK)
					return nil
				},
			}
		}

		r := newTestRouter([]flow{{
			path:      "/multi",
			method:    http.MethodGet,
			streaming: true,
			upstreams: []upstream{makeU("a"), makeU("b")},
		}}, nil, nil)

		req := httptest.NewRequest(http.MethodGet, "/multi", nil)
		rec := httptest.NewRecorder()
		r.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusInternalServerError, rec.Code)
	})
}

func TestStreaming_NonOKUpstreamStatus(t *testing.T) {
	t.Run("preserves the status code", func(t *testing.T) {
		u := &mockProxyUpstream{
			upstreamName: "created",
			proxyFn: func(w http.ResponseWriter, _ *http.Request) error {
				w.WriteHeader(http.StatusCreated)
				return nil
			},
		}

		r := newTestRouter([]flow{streamingFlow("/created", u)}, nil, nil)
		req := httptest.NewRequest(http.MethodGet, "/created", nil)
		rec := httptest.NewRecorder()
		r.ServeHTTP(rec, req)

		assert.Equal(t, http.StatusCreated, rec.Code)
	})
}