Merge pull request #486 from stefanprodan/test-http-api

Improve test coverage of the HTTP API
This commit is contained in:
Stefan Prodan
2026-05-20 11:18:59 +03:00
committed by GitHub
13 changed files with 563 additions and 13 deletions

View File

@@ -22,19 +22,19 @@ type TokenServer struct {
type jwtCustomClaims struct {
Name string `json:"name"`
jwt.StandardClaims
jwt.RegisteredClaims
}
func (s *TokenServer) TokenGenerate(ctx context.Context, req *pb.TokenRequest) (*pb.TokenResponse, error) {
user := "anonymous"
expiresAt := time.Now().Add(time.Minute * 1).Unix()
expiresAt := time.Now().Add(time.Minute * 1)
claims := &jwtCustomClaims{
user,
jwt.StandardClaims{
jwt.RegisteredClaims{
Issuer: "podinfo",
ExpiresAt: expiresAt,
ExpiresAt: jwt.NewNumericDate(expiresAt),
},
}
@@ -48,7 +48,7 @@ func (s *TokenServer) TokenGenerate(ctx context.Context, req *pb.TokenRequest) (
var result = pb.TokenResponse{
Token: t,
ExpiresAt: time.Unix(claims.StandardClaims.ExpiresAt, 0).String(),
ExpiresAt: claims.ExpiresAt.Time.String(),
Message: "Token generated successfully",
}
@@ -88,12 +88,12 @@ func (s *TokenServer) TokenValidate(ctx context.Context, req *pb.TokenRequest) (
}
if parsed_token.Valid {
if claims.StandardClaims.Issuer != "podinfo" {
if claims.Issuer != "podinfo" {
return nil, status.Errorf(codes.OK, "Invalid issuer")
} else {
var result = pb.TokenResponse{
Token: claims.Name,
ExpiresAt: time.Unix(claims.StandardClaims.ExpiresAt, 0).String(),
ExpiresAt: claims.ExpiresAt.Time.String(),
}
return &result, nil
}

View File

@@ -0,0 +1,50 @@
package http
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestCacheWriteHandler_NoPool(t *testing.T) {
srv := NewMockServer()
srv.pool = nil
srv.router.HandleFunc("/cache/{key}", srv.cacheWriteHandler)
req, _ := http.NewRequest("POST", "/cache/key1", strings.NewReader("value1"))
rr := httptest.NewRecorder()
srv.router.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "cache server is offline") {
t.Errorf("expected offline error, got: %s", rr.Body.String())
}
}
func TestCacheDeleteHandler_NoPool(t *testing.T) {
srv := NewMockServer()
srv.pool = nil
srv.router.HandleFunc("/cache/{key}", srv.cacheDeleteHandler).Methods("DELETE")
req, _ := http.NewRequest("DELETE", "/cache/key1", nil)
rr := httptest.NewRecorder()
srv.router.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "cache server is offline") {
t.Errorf("expected offline error, got: %s", rr.Body.String())
}
}
func TestCacheReadHandler_NoPool(t *testing.T) {
srv := NewMockServer()
srv.pool = nil
srv.router.HandleFunc("/cache/{key}", srv.cacheReadHandler).Methods("GET")
req, _ := http.NewRequest("GET", "/cache/key1", nil)
rr := httptest.NewRecorder()
srv.router.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "cache server is offline") {
t.Errorf("expected offline error, got: %s", rr.Body.String())
}
}

View File

@@ -0,0 +1,22 @@
package http
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestConfigReadHandler(t *testing.T) {
srv := NewMockServer()
req, _ := http.NewRequest("GET", "/configs", nil)
rr := httptest.NewRecorder()
http.HandlerFunc(srv.configReadHandler).ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("got status %d, want %d", rr.Code, http.StatusOK)
}
if !strings.Contains(rr.Body.String(), "{}") {
t.Errorf("expected empty JSON object, got: %s", rr.Body.String())
}
}

View File

@@ -33,3 +33,66 @@ func TestDelayHandler(t *testing.T) {
rr.Body.String(), expected)
}
}
func TestRandomDelayMiddleware(t *testing.T) {
m := NewRandomDelayMiddleware(0, 1, "ms")
called := false
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
w.WriteHeader(http.StatusOK)
}))
req, _ := http.NewRequest("GET", "/", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if !called {
t.Error("next handler was not called")
}
if rr.Code != http.StatusOK {
t.Errorf("got status %d, want %d", rr.Code, http.StatusOK)
}
}
func TestRandomDelayMiddleware_Milliseconds(t *testing.T) {
m := NewRandomDelayMiddleware(0, 1, "ms")
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req, _ := http.NewRequest("GET", "/", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("got status %d, want %d", rr.Code, http.StatusOK)
}
}
func TestRandomErrorMiddleware(t *testing.T) {
handler := randomErrorMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
gotOK := false
gotError := false
for i := 0; i < 50; i++ {
req, _ := http.NewRequest("GET", "/", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code == http.StatusOK {
gotOK = true
} else {
gotError = true
}
if gotOK && gotError {
break
}
}
if !gotOK {
t.Error("never got a successful response")
}
if !gotError {
t.Error("never got an error response")
}
}

View File

@@ -74,3 +74,112 @@ func TestEchoHandler_ContentType(t *testing.T) {
}
}
}
func TestCopyTracingHeaders(t *testing.T) {
from, _ := http.NewRequest("GET", "/", nil)
from.Header.Set("x-request-id", "abc123")
from.Header.Set("x-b3-traceid", "trace-1")
from.Header.Set("x-b3-spanid", "span-1")
to, _ := http.NewRequest("POST", "/echo", nil)
copyTracingHeaders(from, to)
if to.Header.Get("x-request-id") != "abc123" {
t.Errorf("x-request-id not copied")
}
if to.Header.Get("x-b3-traceid") != "trace-1" {
t.Errorf("x-b3-traceid not copied")
}
if to.Header.Get("x-b3-spanid") != "span-1" {
t.Errorf("x-b3-spanid not copied")
}
}
func TestEchoHandler_WithBackend(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`"backend response"`))
}))
defer backend.Close()
srv := NewMockServer()
srv.config.BackendURL = []string{backend.URL}
req, _ := http.NewRequest("POST", "/echo", strings.NewReader("hello"))
req.Header.Set("x-request-id", "test-123")
rr := httptest.NewRecorder()
http.HandlerFunc(srv.echoHandler).ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("got status %d, want %d", rr.Code, http.StatusOK)
}
if !strings.Contains(rr.Body.String(), "backend response") {
t.Errorf("expected backend response in body, got: %s", rr.Body.String())
}
if rr.Header().Get("X-Color") != "blue" {
t.Errorf("X-Color = %q, want %q", rr.Header().Get("X-Color"), "blue")
}
}
func TestEchoHandler_BackendError(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer backend.Close()
srv := NewMockServer()
srv.config.BackendURL = []string{backend.URL}
req, _ := http.NewRequest("POST", "/echo", strings.NewReader("hello"))
rr := httptest.NewRecorder()
http.HandlerFunc(srv.echoHandler).ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("got status %d, want %d", rr.Code, http.StatusOK)
}
if !strings.Contains(rr.Body.String(), "response status code 500") {
t.Errorf("expected error message in body, got: %s", rr.Body.String())
}
}
func TestEchoHandler_BackendUnreachable(t *testing.T) {
srv := NewMockServer()
srv.config.BackendURL = []string{"http://127.0.0.1:1"}
req, _ := http.NewRequest("POST", "/echo", strings.NewReader("hello"))
rr := httptest.NewRecorder()
http.HandlerFunc(srv.echoHandler).ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("got status %d, want %d", rr.Code, http.StatusOK)
}
if !strings.Contains(rr.Body.String(), "call failed") {
t.Errorf("expected call failed in body, got: %s", rr.Body.String())
}
}
func TestEchoHandler_MultipleBackends(t *testing.T) {
backend1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`"resp1"`))
}))
defer backend1.Close()
backend2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`"resp2"`))
}))
defer backend2.Close()
srv := NewMockServer()
srv.config.BackendURL = []string{backend1.URL, backend2.URL}
req, _ := http.NewRequest("POST", "/echo", strings.NewReader("hello"))
rr := httptest.NewRecorder()
http.HandlerFunc(srv.echoHandler).ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("got status %d, want %d", rr.Code, http.StatusOK)
}
if !strings.Contains(rr.Body.String(), "resp1") || !strings.Contains(rr.Body.String(), "resp2") {
t.Errorf("expected both backend responses, got: %s", rr.Body.String())
}
}

View File

@@ -0,0 +1,36 @@
package http
import (
"net/http/httptest"
"strings"
"testing"
"github.com/gorilla/websocket"
)
func TestEchoWsHandler(t *testing.T) {
srv := NewMockServer()
srv.router.HandleFunc("/ws/echo", srv.echoWsHandler)
server := httptest.NewServer(srv.router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws/echo"
ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("websocket dial failed: %v", err)
}
defer ws.Close()
msg := "hello websocket"
if err := ws.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil {
t.Fatalf("write failed: %v", err)
}
_, p, err := ws.ReadMessage()
if err != nil {
t.Fatalf("read failed: %v", err)
}
if len(p) == 0 {
t.Error("received empty message")
}
}

View File

@@ -33,3 +33,17 @@ func TestEnvHandler(t *testing.T) {
rr.Body.String(), expected)
}
}
func TestEnvHandler_Actual(t *testing.T) {
srv := NewMockServer()
req, _ := http.NewRequest("GET", "/env", nil)
rr := httptest.NewRecorder()
http.HandlerFunc(srv.envHandler).ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("got status %d, want %d", rr.Code, http.StatusOK)
}
if rr.Header().Get("Content-Type") != "application/json; charset=utf-8" {
t.Errorf("Content-Type = %q, want application/json", rr.Header().Get("Content-Type"))
}
}

View File

@@ -3,6 +3,8 @@ package http
import (
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
)
@@ -43,3 +45,67 @@ func TestReadyzHandler(t *testing.T) {
status, http.StatusServiceUnavailable)
}
}
func TestHealthzHandler_Healthy(t *testing.T) {
srv := NewMockServer()
atomic.StoreInt32(&healthy, 1)
defer atomic.StoreInt32(&healthy, 0)
req, _ := http.NewRequest("GET", "/healthz", nil)
rr := httptest.NewRecorder()
http.HandlerFunc(srv.healthzHandler).ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("got status %d, want %d", rr.Code, http.StatusOK)
}
if !strings.Contains(rr.Body.String(), "OK") {
t.Errorf("expected OK in body, got: %s", rr.Body.String())
}
}
func TestReadyzHandler_Ready(t *testing.T) {
srv := NewMockServer()
atomic.StoreInt32(&ready, 1)
defer atomic.StoreInt32(&ready, 0)
req, _ := http.NewRequest("GET", "/readyz", nil)
rr := httptest.NewRecorder()
http.HandlerFunc(srv.readyzHandler).ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("got status %d, want %d", rr.Code, http.StatusOK)
}
}
func TestEnableReadyHandler(t *testing.T) {
srv := NewMockServer()
atomic.StoreInt32(&ready, 0)
req, _ := http.NewRequest("POST", "/readyz/enable", nil)
rr := httptest.NewRecorder()
http.HandlerFunc(srv.enableReadyHandler).ServeHTTP(rr, req)
if rr.Code != http.StatusAccepted {
t.Fatalf("got status %d, want %d", rr.Code, http.StatusAccepted)
}
if atomic.LoadInt32(&ready) != 1 {
t.Error("ready flag not set to 1")
}
atomic.StoreInt32(&ready, 0)
}
func TestDisableReadyHandler(t *testing.T) {
srv := NewMockServer()
atomic.StoreInt32(&ready, 1)
req, _ := http.NewRequest("POST", "/readyz/disable", nil)
rr := httptest.NewRecorder()
http.HandlerFunc(srv.disableReadyHandler).ServeHTTP(rr, req)
if rr.Code != http.StatusAccepted {
t.Fatalf("got status %d, want %d", rr.Code, http.StatusAccepted)
}
if atomic.LoadInt32(&ready) != 0 {
t.Error("ready flag not set to 0")
}
}

23
pkg/api/http/http_test.go Normal file
View File

@@ -0,0 +1,23 @@
package http
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestVersionMiddleware(t *testing.T) {
srv := NewMockServer()
handler := versionMiddleware(http.HandlerFunc(srv.infoHandler))
req, _ := http.NewRequest("GET", "/", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if req.Header.Get("X-API-Version") == "" {
t.Error("X-API-Version not set by middleware")
}
if req.Header.Get("X-API-Revision") == "" {
t.Error("X-API-Revision not set by middleware")
}
}

View File

@@ -24,3 +24,28 @@ func TestStatusHandler(t *testing.T) {
status, http.StatusNotFound)
}
}
func TestStatusHandler_Various(t *testing.T) {
srv := NewMockServer()
srv.router.HandleFunc("/status/{code}", srv.statusHandler)
cases := []struct {
code string
expected int
}{
{"200", 200},
{"201", 201},
{"500", 500},
{"503", 503},
}
for _, c := range cases {
req, _ := http.NewRequest("GET", "/status/"+c.code, nil)
rr := httptest.NewRecorder()
srv.router.ServeHTTP(rr, req)
if rr.Code != c.expected {
t.Errorf("/status/%s: got %d, want %d", c.code, rr.Code, c.expected)
}
}
}

View File

@@ -1,6 +1,7 @@
package http
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
@@ -80,3 +81,50 @@ func TestStoreReadHandler_PathTraversal(t *testing.T) {
}
}
}
func TestStoreWriteHandler(t *testing.T) {
srv := NewMockServer()
srv.config.DataPath = t.TempDir()
payload := "hello world"
req, _ := http.NewRequest("POST", "/store", strings.NewReader(payload))
rr := httptest.NewRecorder()
http.HandlerFunc(srv.storeWriteHandler).ServeHTTP(rr, req)
if rr.Code != http.StatusAccepted {
t.Fatalf("got status %d, want %d", rr.Code, http.StatusAccepted)
}
var result map[string]string
json.Unmarshal(rr.Body.Bytes(), &result)
if result["hash"] != hash(payload) {
t.Errorf("hash = %q, want %q", result["hash"], hash(payload))
}
}
func TestStoreWriteHandler_InvalidPath(t *testing.T) {
srv := NewMockServer()
srv.config.DataPath = "/nonexistent/path/that/does/not/exist"
req, _ := http.NewRequest("POST", "/store", strings.NewReader("data"))
rr := httptest.NewRecorder()
http.HandlerFunc(srv.storeWriteHandler).ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "writing file failed") {
t.Errorf("expected write error, got: %s", rr.Body.String())
}
}
func TestStoreReadHandler_NotFound(t *testing.T) {
srv := NewMockServer()
srv.config.DataPath = t.TempDir()
srv.router.HandleFunc("/store/{hash}", srv.storeReadHandler)
req, _ := http.NewRequest("GET", "/store/aabbccddee11223344556677889900aabbccddee", nil)
rr := httptest.NewRecorder()
srv.router.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "reading file failed") {
t.Errorf("expected read error, got: %s", rr.Body.String())
}
}

View File

@@ -13,7 +13,7 @@ import (
type jwtCustomClaims struct {
Name string `json:"name"`
jwt.StandardClaims
jwt.RegisteredClaims
}
// Token godoc
@@ -44,9 +44,9 @@ func (s *Server) tokenGenerateHandler(w http.ResponseWriter, r *http.Request) {
expiresAt := time.Now().Add(time.Minute * 1)
claims := &jwtCustomClaims{
user,
jwt.StandardClaims{
jwt.RegisteredClaims{
Issuer: "podinfo",
ExpiresAt: expiresAt.Unix(),
ExpiresAt: jwt.NewNumericDate(expiresAt),
},
}
@@ -59,7 +59,7 @@ func (s *Server) tokenGenerateHandler(w http.ResponseWriter, r *http.Request) {
var result = TokenResponse{
Token: t,
ExpiresAt: time.Unix(claims.StandardClaims.ExpiresAt, 0),
ExpiresAt: claims.ExpiresAt.Time,
}
s.JSONResponse(w, r, result)
@@ -104,12 +104,12 @@ func (s *Server) tokenValidateHandler(w http.ResponseWriter, r *http.Request) {
}
if token.Valid {
if claims.StandardClaims.Issuer != "podinfo" {
if claims.Issuer != "podinfo" {
s.ErrorResponse(w, r, span, "invalid issuer", http.StatusUnauthorized)
} else {
var result = TokenValidationResponse{
TokenName: claims.Name,
ExpiresAt: time.Unix(claims.StandardClaims.ExpiresAt, 0),
ExpiresAt: claims.ExpiresAt.Time,
}
s.JSONResponse(w, r, result)
}

View File

@@ -34,3 +34,97 @@ func TestTokenHandler(t *testing.T) {
t.Error("handler returned no token")
}
}
func TestTokenGenerateHandler_EmptyBody(t *testing.T) {
srv := NewMockServer()
srv.config.JWTSecret = "test-secret"
req, _ := http.NewRequest("POST", "/token", strings.NewReader(""))
rr := httptest.NewRecorder()
http.HandlerFunc(srv.tokenGenerateHandler).ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("got status %d, want %d", rr.Code, http.StatusOK)
}
var result TokenResponse
json.Unmarshal(rr.Body.Bytes(), &result)
if result.Token == "" {
t.Error("expected non-empty token")
}
}
func TestTokenValidateHandler(t *testing.T) {
srv := NewMockServer()
srv.config.JWTSecret = "test-secret"
// Generate a token first
genReq, _ := http.NewRequest("POST", "/token", strings.NewReader("test-user"))
genRR := httptest.NewRecorder()
http.HandlerFunc(srv.tokenGenerateHandler).ServeHTTP(genRR, genReq)
var tokenResp TokenResponse
json.Unmarshal(genRR.Body.Bytes(), &tokenResp)
t.Run("valid token", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/token/validate", nil)
req.Header.Set("Authorization", "Bearer "+tokenResp.Token)
rr := httptest.NewRecorder()
http.HandlerFunc(srv.tokenValidateHandler).ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("got status %d, want %d", rr.Code, http.StatusOK)
}
var result TokenValidationResponse
json.Unmarshal(rr.Body.Bytes(), &result)
if result.TokenName != "test-user" {
t.Errorf("token_name = %q, want %q", result.TokenName, "test-user")
}
})
t.Run("missing authorization header", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/token/validate", nil)
rr := httptest.NewRecorder()
http.HandlerFunc(srv.tokenValidateHandler).ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "authorization bearer header required") {
t.Errorf("unexpected body: %s", rr.Body.String())
}
})
t.Run("malformed authorization header", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/token/validate", nil)
req.Header.Set("Authorization", "InvalidFormat")
rr := httptest.NewRecorder()
http.HandlerFunc(srv.tokenValidateHandler).ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "authorization bearer header required") {
t.Errorf("unexpected body: %s", rr.Body.String())
}
})
t.Run("invalid token", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/token/validate", nil)
req.Header.Set("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZmFrZSIsImlzcyI6InBvZGluZm8iLCJleHAiOjk5OTk5OTk5OTl9.invalidsig")
rr := httptest.NewRecorder()
http.HandlerFunc(srv.tokenValidateHandler).ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("got status %d", rr.Code)
}
})
t.Run("wrong signing secret", func(t *testing.T) {
otherSrv := NewMockServer()
otherSrv.config.JWTSecret = "different-secret"
req, _ := http.NewRequest("GET", "/token/validate", nil)
req.Header.Set("Authorization", "Bearer "+tokenResp.Token)
rr := httptest.NewRecorder()
http.HandlerFunc(otherSrv.tokenValidateHandler).ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "signature is invalid") {
t.Errorf("expected signature error, got: %s", rr.Body.String())
}
})
}