feat: migrate receiver to Fuego framework with OpenAPI generation
All checks were successful
ci / ci (push) Successful in 2m2s
ci / goreleaser (push) Successful in 2m29s

Replace net/http handlers with Fuego framework for automatic OpenAPI 3.0
spec generation. Add generated Go client package, OpenAPI extraction
script, and update Makefile with separate build/run targets for both
binaries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Manuel Ganter 2026-02-18 11:12:14 +01:00
parent 479c13f596
commit bc9d0dd8ea
No known key found for this signature in database
11 changed files with 2245 additions and 252 deletions

View file

@ -11,6 +11,8 @@ import (
"strings"
"testing"
"github.com/go-fuego/fuego"
"edp.buildth.ing/DevFW-CICD/forgejo-runner-sizer/internal/summary"
)
@ -42,9 +44,8 @@ func TestHandler_ReceiveMetrics(t *testing.T) {
req.Header.Set("Authorization", "Bearer "+pushToken)
rec := httptest.NewRecorder()
mux := http.NewServeMux()
h.RegisterRoutes(mux)
mux.ServeHTTP(rec, req)
s := newTestServer(h)
s.Mux.ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Errorf("status = %d, want %d", rec.Code, http.StatusCreated)
@ -69,9 +70,8 @@ func TestHandler_ReceiveMetrics_InvalidJSON(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/v1/metrics", bytes.NewReader([]byte("not json")))
rec := httptest.NewRecorder()
mux := http.NewServeMux()
h.RegisterRoutes(mux)
mux.ServeHTTP(rec, req)
s := newTestServer(h)
s.Mux.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
@ -95,9 +95,8 @@ func TestHandler_ReceiveMetrics_MissingRunID(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/v1/metrics", bytes.NewReader(body))
rec := httptest.NewRecorder()
mux := http.NewServeMux()
h.RegisterRoutes(mux)
mux.ServeHTTP(rec, req)
s := newTestServer(h)
s.Mux.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
@ -125,9 +124,8 @@ func TestHandler_GetByWorkflowJob(t *testing.T) {
req.Header.Set("Authorization", "Bearer "+readToken)
rec := httptest.NewRecorder()
mux := http.NewServeMux()
h.RegisterRoutes(mux)
mux.ServeHTTP(rec, req)
s := newTestServer(h)
s.Mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
@ -151,9 +149,8 @@ func TestHandler_GetByWorkflowJob_NotFound(t *testing.T) {
req.Header.Set("Authorization", "Bearer "+readToken)
rec := httptest.NewRecorder()
mux := http.NewServeMux()
h.RegisterRoutes(mux)
mux.ServeHTTP(rec, req)
s := newTestServer(h)
s.Mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
@ -180,8 +177,7 @@ func TestHandler_GetByWorkflowJob_WithToken(t *testing.T) {
t.Fatalf("SaveMetric() error = %v", err)
}
mux := http.NewServeMux()
h.RegisterRoutes(mux)
s := newTestServer(h)
tests := []struct {
name string
@ -201,7 +197,7 @@ func TestHandler_GetByWorkflowJob_WithToken(t *testing.T) {
req.Header.Set("Authorization", tt.authHeader)
}
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
s.Mux.ServeHTTP(rec, req)
if rec.Code != tt.wantCode {
t.Errorf("status = %d, want %d", rec.Code, tt.wantCode)
@ -217,9 +213,8 @@ func TestHandler_Health(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rec := httptest.NewRecorder()
mux := http.NewServeMux()
h.RegisterRoutes(mux)
mux.ServeHTTP(rec, req)
s := newTestServer(h)
s.Mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
@ -250,9 +245,8 @@ func TestHandler_GenerateToken(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
mux := http.NewServeMux()
h.RegisterRoutes(mux)
mux.ServeHTTP(rec, req)
s := newTestServer(h)
s.Mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
@ -289,9 +283,8 @@ func TestHandler_GenerateToken_NoAuth(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/v1/token", bytes.NewReader(body))
rec := httptest.NewRecorder()
mux := http.NewServeMux()
h.RegisterRoutes(mux)
mux.ServeHTTP(rec, req)
s := newTestServer(h)
s.Mux.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("status = %d, want %d", rec.Code, http.StatusUnauthorized)
@ -314,9 +307,8 @@ func TestHandler_GenerateToken_MissingFields(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
mux := http.NewServeMux()
h.RegisterRoutes(mux)
mux.ServeHTTP(rec, req)
s := newTestServer(h)
s.Mux.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
@ -338,12 +330,12 @@ func TestHandler_GenerateToken_NoReadToken(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
mux := http.NewServeMux()
h.RegisterRoutes(mux)
mux.ServeHTTP(rec, req)
s := newTestServer(h)
s.Mux.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
// With no read token, the middleware rejects before we reach the handler
if rec.Code != http.StatusUnauthorized {
t.Errorf("status = %d, want %d", rec.Code, http.StatusUnauthorized)
}
}
@ -352,8 +344,7 @@ func TestHandler_ReceiveMetrics_WithPushToken(t *testing.T) {
h, cleanup := newTestHandlerWithToken(t, readToken)
defer cleanup()
mux := http.NewServeMux()
h.RegisterRoutes(mux)
s := newTestServer(h)
exec := ExecutionContext{
Organization: "org",
@ -391,7 +382,7 @@ func TestHandler_ReceiveMetrics_WithPushToken(t *testing.T) {
req.Header.Set("Authorization", tt.authHeader)
}
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
s.Mux.ServeHTTP(rec, req)
if rec.Code != tt.wantCode {
t.Errorf("status = %d, want %d", rec.Code, tt.wantCode)
@ -420,9 +411,8 @@ func TestHandler_ReceiveMetrics_RejectsWhenNoReadToken(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
mux := http.NewServeMux()
h.RegisterRoutes(mux)
mux.ServeHTTP(rec, req)
s := newTestServer(h)
s.Mux.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("status = %d, want %d", rec.Code, http.StatusUnauthorized)
@ -436,15 +426,27 @@ func TestHandler_GetByWorkflowJob_RejectsWhenNoReadToken(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/v1/metrics/repo/org/repo/ci.yml/build", nil)
rec := httptest.NewRecorder()
mux := http.NewServeMux()
h.RegisterRoutes(mux)
mux.ServeHTTP(rec, req)
s := newTestServer(h)
s.Mux.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("status = %d, want %d", rec.Code, http.StatusUnauthorized)
}
}
func newTestServer(h *Handler) *fuego.Server {
s := fuego.NewServer(
fuego.WithoutStartupMessages(),
fuego.WithEngineOptions(
fuego.WithOpenAPIConfig(fuego.OpenAPIConfig{
Disabled: true,
}),
),
)
h.RegisterRoutes(s)
return s
}
func newTestHandler(t *testing.T) (*Handler, func()) {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "test.db")