mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-01 04:50:16 +00:00
Tests for the responseWriter struct in middleware.go: - APIError.Error() interface implementation (3 cases) - WriteHeader once-only semantics (4 cases) - Write with implicit WriteHeader (3 cases) - StatusCode nil safety (4 cases) - Hijack support detection (2 cases) - Flush support detection (3 cases) - Full flow integration and edge cases (4 cases) First test file for middleware.go. 22 test cases total covering HTTP response wrapper behavior used by ErrorHandler middleware.
443 lines
11 KiB
Go
443 lines
11 KiB
Go
package api
|
|
|
|
import (
|
|
"bufio"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func TestAPIError_Error(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
apiError APIError
|
|
want string
|
|
}{
|
|
{
|
|
name: "simple error message",
|
|
apiError: APIError{ErrorMessage: "something went wrong"},
|
|
want: "something went wrong",
|
|
},
|
|
{
|
|
name: "empty error message",
|
|
apiError: APIError{ErrorMessage: ""},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "error with all fields",
|
|
apiError: APIError{
|
|
ErrorMessage: "unauthorized",
|
|
Code: "AUTH_FAILED",
|
|
StatusCode: 401,
|
|
Timestamp: 1234567890,
|
|
RequestID: "req-123",
|
|
Details: map[string]string{"reason": "invalid token"},
|
|
},
|
|
want: "unauthorized",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.apiError.Error()
|
|
if got != tt.want {
|
|
t.Errorf("APIError.Error() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAPIError_ImplementsError(t *testing.T) {
|
|
var _ error = &APIError{}
|
|
}
|
|
|
|
func TestResponseWriter_WriteHeader(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
codes []int
|
|
wantStatusCode int
|
|
wantWrittenCount int
|
|
wantUnderlyingCode int
|
|
}{
|
|
{
|
|
name: "single write",
|
|
codes: []int{http.StatusOK},
|
|
wantStatusCode: http.StatusOK,
|
|
wantWrittenCount: 1,
|
|
wantUnderlyingCode: http.StatusOK,
|
|
},
|
|
{
|
|
name: "first write wins",
|
|
codes: []int{http.StatusCreated, http.StatusBadRequest, http.StatusInternalServerError},
|
|
wantStatusCode: http.StatusCreated,
|
|
wantWrittenCount: 1,
|
|
wantUnderlyingCode: http.StatusCreated,
|
|
},
|
|
{
|
|
name: "error code",
|
|
codes: []int{http.StatusNotFound},
|
|
wantStatusCode: http.StatusNotFound,
|
|
wantWrittenCount: 1,
|
|
wantUnderlyingCode: http.StatusNotFound,
|
|
},
|
|
{
|
|
name: "server error",
|
|
codes: []int{http.StatusInternalServerError},
|
|
wantStatusCode: http.StatusInternalServerError,
|
|
wantWrittenCount: 1,
|
|
wantUnderlyingCode: http.StatusInternalServerError,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
rw := &responseWriter{ResponseWriter: rec, statusCode: http.StatusOK}
|
|
|
|
for _, code := range tt.codes {
|
|
rw.WriteHeader(code)
|
|
}
|
|
|
|
if rw.statusCode != tt.wantStatusCode {
|
|
t.Errorf("statusCode = %d, want %d", rw.statusCode, tt.wantStatusCode)
|
|
}
|
|
|
|
if rec.Code != tt.wantUnderlyingCode {
|
|
t.Errorf("underlying Code = %d, want %d", rec.Code, tt.wantUnderlyingCode)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResponseWriter_WriteHeader_WrittenFlag(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
rw := &responseWriter{ResponseWriter: rec, statusCode: http.StatusOK}
|
|
|
|
if rw.written {
|
|
t.Error("written should be false initially")
|
|
}
|
|
|
|
rw.WriteHeader(http.StatusCreated)
|
|
|
|
if !rw.written {
|
|
t.Error("written should be true after WriteHeader")
|
|
}
|
|
}
|
|
|
|
func TestResponseWriter_Write(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
preWriteHeader bool
|
|
preWriteHeaderCode int
|
|
writeData []byte
|
|
wantStatusCode int
|
|
wantWritten bool
|
|
}{
|
|
{
|
|
name: "write without prior WriteHeader",
|
|
preWriteHeader: false,
|
|
writeData: []byte("hello"),
|
|
wantStatusCode: http.StatusOK,
|
|
wantWritten: true,
|
|
},
|
|
{
|
|
name: "write with prior WriteHeader",
|
|
preWriteHeader: true,
|
|
preWriteHeaderCode: http.StatusCreated,
|
|
writeData: []byte("created"),
|
|
wantStatusCode: http.StatusCreated,
|
|
wantWritten: true,
|
|
},
|
|
{
|
|
name: "empty write without prior WriteHeader",
|
|
preWriteHeader: false,
|
|
writeData: []byte{},
|
|
wantStatusCode: http.StatusOK,
|
|
wantWritten: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
rw := &responseWriter{ResponseWriter: rec, statusCode: http.StatusOK}
|
|
|
|
if tt.preWriteHeader {
|
|
rw.WriteHeader(tt.preWriteHeaderCode)
|
|
}
|
|
|
|
n, err := rw.Write(tt.writeData)
|
|
if err != nil {
|
|
t.Fatalf("Write() error = %v", err)
|
|
}
|
|
|
|
if n != len(tt.writeData) {
|
|
t.Errorf("Write() = %d bytes, want %d", n, len(tt.writeData))
|
|
}
|
|
|
|
if rw.statusCode != tt.wantStatusCode {
|
|
t.Errorf("statusCode = %d, want %d", rw.statusCode, tt.wantStatusCode)
|
|
}
|
|
|
|
if rw.written != tt.wantWritten {
|
|
t.Errorf("written = %v, want %v", rw.written, tt.wantWritten)
|
|
}
|
|
|
|
if string(rec.Body.Bytes()) != string(tt.writeData) {
|
|
t.Errorf("body = %q, want %q", rec.Body.String(), string(tt.writeData))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResponseWriter_Write_MultipleWrites(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
rw := &responseWriter{ResponseWriter: rec, statusCode: http.StatusOK}
|
|
|
|
// First write triggers implicit WriteHeader
|
|
_, err := rw.Write([]byte("first"))
|
|
if err != nil {
|
|
t.Fatalf("first Write() error = %v", err)
|
|
}
|
|
|
|
// Second write should not change status
|
|
_, err = rw.Write([]byte(" second"))
|
|
if err != nil {
|
|
t.Fatalf("second Write() error = %v", err)
|
|
}
|
|
|
|
if rw.statusCode != http.StatusOK {
|
|
t.Errorf("statusCode = %d, want %d", rw.statusCode, http.StatusOK)
|
|
}
|
|
|
|
if rec.Body.String() != "first second" {
|
|
t.Errorf("body = %q, want %q", rec.Body.String(), "first second")
|
|
}
|
|
}
|
|
|
|
func TestResponseWriter_StatusCode(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rw *responseWriter
|
|
wantStatusCode int
|
|
}{
|
|
{
|
|
name: "nil receiver",
|
|
rw: nil,
|
|
wantStatusCode: http.StatusInternalServerError,
|
|
},
|
|
{
|
|
name: "default status",
|
|
rw: &responseWriter{statusCode: http.StatusOK},
|
|
wantStatusCode: http.StatusOK,
|
|
},
|
|
{
|
|
name: "custom status",
|
|
rw: &responseWriter{statusCode: http.StatusNotFound},
|
|
wantStatusCode: http.StatusNotFound,
|
|
},
|
|
{
|
|
name: "server error status",
|
|
rw: &responseWriter{statusCode: http.StatusBadGateway},
|
|
wantStatusCode: http.StatusBadGateway,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.rw.StatusCode()
|
|
if got != tt.wantStatusCode {
|
|
t.Errorf("StatusCode() = %d, want %d", got, tt.wantStatusCode)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// mockHijacker implements http.Hijacker for testing
|
|
type mockHijacker struct {
|
|
http.ResponseWriter
|
|
conn net.Conn
|
|
rw *bufio.ReadWriter
|
|
err error
|
|
}
|
|
|
|
func (m *mockHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|
return m.conn, m.rw, m.err
|
|
}
|
|
|
|
func TestResponseWriter_Hijack_NotSupported(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
rw := &responseWriter{ResponseWriter: rec, statusCode: http.StatusOK}
|
|
|
|
conn, brw, err := rw.Hijack()
|
|
if err == nil {
|
|
t.Error("Hijack() should return error when underlying writer doesn't support it")
|
|
}
|
|
if conn != nil {
|
|
t.Error("Hijack() should return nil conn on error")
|
|
}
|
|
if brw != nil {
|
|
t.Error("Hijack() should return nil bufio.ReadWriter on error")
|
|
}
|
|
if err.Error() != "ResponseWriter does not implement http.Hijacker" {
|
|
t.Errorf("Hijack() error = %q, want specific error message", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestResponseWriter_Hijack_Supported(t *testing.T) {
|
|
// Create a mock hijacker that supports hijacking
|
|
mockConn := &net.TCPConn{}
|
|
mockRW := bufio.NewReadWriter(bufio.NewReader(nil), bufio.NewWriter(nil))
|
|
hijacker := &mockHijacker{
|
|
ResponseWriter: httptest.NewRecorder(),
|
|
conn: mockConn,
|
|
rw: mockRW,
|
|
err: nil,
|
|
}
|
|
|
|
rw := &responseWriter{ResponseWriter: hijacker, statusCode: http.StatusOK}
|
|
|
|
conn, brw, err := rw.Hijack()
|
|
if err != nil {
|
|
t.Errorf("Hijack() error = %v, want nil", err)
|
|
}
|
|
if conn != mockConn {
|
|
t.Error("Hijack() returned unexpected conn")
|
|
}
|
|
if brw != mockRW {
|
|
t.Error("Hijack() returned unexpected bufio.ReadWriter")
|
|
}
|
|
}
|
|
|
|
// mockFlusher implements http.Flusher for testing
|
|
type mockFlusher struct {
|
|
http.ResponseWriter
|
|
flushed bool
|
|
}
|
|
|
|
func (m *mockFlusher) Flush() {
|
|
m.flushed = true
|
|
}
|
|
|
|
func TestResponseWriter_Flush_NotSupported(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
rw := &responseWriter{ResponseWriter: rec, statusCode: http.StatusOK}
|
|
|
|
// This should not panic even though underlying doesn't support Flush
|
|
rw.Flush()
|
|
}
|
|
|
|
func TestResponseWriter_Flush_Supported(t *testing.T) {
|
|
flusher := &mockFlusher{ResponseWriter: httptest.NewRecorder()}
|
|
rw := &responseWriter{ResponseWriter: flusher, statusCode: http.StatusOK}
|
|
|
|
if flusher.flushed {
|
|
t.Error("flushed should be false initially")
|
|
}
|
|
|
|
rw.Flush()
|
|
|
|
if !flusher.flushed {
|
|
t.Error("Flush() should call underlying Flusher.Flush()")
|
|
}
|
|
}
|
|
|
|
// Note: httptest.ResponseRecorder implements http.Flusher
|
|
func TestResponseWriter_Flush_WithRecorder(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
rw := &responseWriter{ResponseWriter: rec, statusCode: http.StatusOK}
|
|
|
|
// Write some data
|
|
rw.Write([]byte("test"))
|
|
|
|
// Flush should work with httptest.ResponseRecorder (it implements Flusher)
|
|
rw.Flush()
|
|
|
|
// If we got here without panic, the test passes
|
|
}
|
|
|
|
func TestResponseWriter_Header(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
rw := &responseWriter{ResponseWriter: rec, statusCode: http.StatusOK}
|
|
|
|
// Set a header through the wrapper
|
|
rw.Header().Set("X-Custom-Header", "test-value")
|
|
|
|
// Verify it was set on the underlying writer
|
|
if got := rec.Header().Get("X-Custom-Header"); got != "test-value" {
|
|
t.Errorf("Header().Get() = %q, want %q", got, "test-value")
|
|
}
|
|
}
|
|
|
|
func TestResponseWriter_FullFlow(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
rw := &responseWriter{ResponseWriter: rec, statusCode: http.StatusOK}
|
|
|
|
// Set headers
|
|
rw.Header().Set("Content-Type", "application/json")
|
|
rw.Header().Set("X-Request-ID", "test-123")
|
|
|
|
// Write status
|
|
rw.WriteHeader(http.StatusCreated)
|
|
|
|
// Write body
|
|
n, err := rw.Write([]byte(`{"status":"created"}`))
|
|
if err != nil {
|
|
t.Fatalf("Write() error = %v", err)
|
|
}
|
|
if n != 20 {
|
|
t.Errorf("Write() = %d bytes, want 20", n)
|
|
}
|
|
|
|
// Verify all values
|
|
if rw.StatusCode() != http.StatusCreated {
|
|
t.Errorf("StatusCode() = %d, want %d", rw.StatusCode(), http.StatusCreated)
|
|
}
|
|
if rec.Code != http.StatusCreated {
|
|
t.Errorf("rec.Code = %d, want %d", rec.Code, http.StatusCreated)
|
|
}
|
|
if rec.Body.String() != `{"status":"created"}` {
|
|
t.Errorf("body = %q, want %q", rec.Body.String(), `{"status":"created"}`)
|
|
}
|
|
if rec.Header().Get("Content-Type") != "application/json" {
|
|
t.Errorf("Content-Type = %q, want %q", rec.Header().Get("Content-Type"), "application/json")
|
|
}
|
|
}
|
|
|
|
func TestResponseWriter_EdgeCases(t *testing.T) {
|
|
t.Run("zero status code preserved", func(t *testing.T) {
|
|
rw := &responseWriter{statusCode: 0}
|
|
if rw.StatusCode() != 0 {
|
|
t.Errorf("StatusCode() = %d, want 0", rw.StatusCode())
|
|
}
|
|
})
|
|
|
|
t.Run("write after write maintains written flag", func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
rw := &responseWriter{ResponseWriter: rec, statusCode: http.StatusOK}
|
|
|
|
rw.Write([]byte("a"))
|
|
if !rw.written {
|
|
t.Error("written should be true after first Write")
|
|
}
|
|
|
|
rw.Write([]byte("b"))
|
|
if !rw.written {
|
|
t.Error("written should remain true after second Write")
|
|
}
|
|
})
|
|
|
|
t.Run("WriteHeader after Write is no-op", func(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
rw := &responseWriter{ResponseWriter: rec, statusCode: http.StatusOK}
|
|
|
|
rw.Write([]byte("data"))
|
|
rw.WriteHeader(http.StatusNotFound)
|
|
|
|
// Status should remain 200 since Write triggered implicit WriteHeader
|
|
if rw.statusCode != http.StatusOK {
|
|
t.Errorf("statusCode = %d, want %d", rw.statusCode, http.StatusOK)
|
|
}
|
|
})
|
|
}
|