2026-02-20 15:44:58 +00:00
|
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
|
|
|
|
|
|
package api_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
|
|
|
|
api "forge.lthn.ai/core/go-api"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ── Stub implementations ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
type stubGroup struct{}
|
|
|
|
|
|
|
|
|
|
func (s *stubGroup) Name() string { return "stub" }
|
|
|
|
|
func (s *stubGroup) BasePath() string { return "/stub" }
|
|
|
|
|
func (s *stubGroup) RegisterRoutes(rg *gin.RouterGroup) {
|
|
|
|
|
rg.GET("/ping", func(c *gin.Context) {
|
|
|
|
|
c.JSON(http.StatusOK, api.OK("pong"))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type stubStreamGroup struct {
|
|
|
|
|
stubGroup
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *stubStreamGroup) Channels() []string {
|
|
|
|
|
return []string{"events", "updates"}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── RouteGroup interface ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestRouteGroup_Good_InterfaceSatisfaction(t *testing.T) {
|
|
|
|
|
var g api.RouteGroup = &stubGroup{}
|
|
|
|
|
|
|
|
|
|
if g.Name() != "stub" {
|
|
|
|
|
t.Fatalf("expected Name=%q, got %q", "stub", g.Name())
|
|
|
|
|
}
|
|
|
|
|
if g.BasePath() != "/stub" {
|
|
|
|
|
t.Fatalf("expected BasePath=%q, got %q", "/stub", g.BasePath())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRouteGroup_Good_RegisterRoutes(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
engine := gin.New()
|
|
|
|
|
|
|
|
|
|
g := &stubGroup{}
|
|
|
|
|
rg := engine.Group(g.BasePath())
|
|
|
|
|
g.RegisterRoutes(rg)
|
|
|
|
|
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, "/stub/ping", nil)
|
|
|
|
|
engine.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("expected status 200, got %d", w.Code)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── StreamGroup interface ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestStreamGroup_Good_InterfaceSatisfaction(t *testing.T) {
|
|
|
|
|
var g api.StreamGroup = &stubStreamGroup{}
|
|
|
|
|
|
|
|
|
|
channels := g.Channels()
|
|
|
|
|
if len(channels) != 2 {
|
|
|
|
|
t.Fatalf("expected 2 channels, got %d", len(channels))
|
|
|
|
|
}
|
|
|
|
|
if channels[0] != "events" {
|
|
|
|
|
t.Fatalf("expected channels[0]=%q, got %q", "events", channels[0])
|
|
|
|
|
}
|
|
|
|
|
if channels[1] != "updates" {
|
|
|
|
|
t.Fatalf("expected channels[1]=%q, got %q", "updates", channels[1])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestStreamGroup_Good_AlsoSatisfiesRouteGroup(t *testing.T) {
|
|
|
|
|
sg := &stubStreamGroup{}
|
|
|
|
|
|
|
|
|
|
// A StreamGroup's embedded stubGroup should also satisfy RouteGroup.
|
|
|
|
|
var rg api.RouteGroup = sg
|
|
|
|
|
if rg.Name() != "stub" {
|
|
|
|
|
t.Fatalf("expected Name=%q, got %q", "stub", rg.Name())
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-21 00:42:13 +00:00
|
|
|
|
|
|
|
|
// ── DescribableGroup interface ────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
// describableStub implements DescribableGroup for testing.
|
|
|
|
|
type describableStub struct {
|
|
|
|
|
stubGroup
|
|
|
|
|
descriptions []api.RouteDescription
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (d *describableStub) Describe() []api.RouteDescription {
|
|
|
|
|
return d.descriptions
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDescribableGroup_Good_ImplementsRouteGroup(t *testing.T) {
|
|
|
|
|
stub := &describableStub{}
|
|
|
|
|
|
|
|
|
|
// Must satisfy DescribableGroup.
|
|
|
|
|
var dg api.DescribableGroup = stub
|
|
|
|
|
if dg.Name() != "stub" {
|
|
|
|
|
t.Fatalf("expected Name=%q, got %q", "stub", dg.Name())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Must also satisfy RouteGroup since DescribableGroup embeds it.
|
|
|
|
|
var rg api.RouteGroup = stub
|
|
|
|
|
if rg.BasePath() != "/stub" {
|
|
|
|
|
t.Fatalf("expected BasePath=%q, got %q", "/stub", rg.BasePath())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDescribableGroup_Good_DescribeReturnsRoutes(t *testing.T) {
|
|
|
|
|
stub := &describableStub{
|
|
|
|
|
descriptions: []api.RouteDescription{
|
|
|
|
|
{
|
|
|
|
|
Method: "GET",
|
|
|
|
|
Path: "/items",
|
|
|
|
|
Summary: "List items",
|
|
|
|
|
Tags: []string{"items"},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Method: "POST",
|
|
|
|
|
Path: "/items",
|
|
|
|
|
Summary: "Create item",
|
|
|
|
|
Tags: []string{"items"},
|
|
|
|
|
RequestBody: map[string]any{
|
|
|
|
|
"type": "object",
|
|
|
|
|
"properties": map[string]any{
|
|
|
|
|
"name": map[string]any{"type": "string"},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var dg api.DescribableGroup = stub
|
|
|
|
|
descs := dg.Describe()
|
|
|
|
|
|
|
|
|
|
if len(descs) != 2 {
|
|
|
|
|
t.Fatalf("expected 2 descriptions, got %d", len(descs))
|
|
|
|
|
}
|
|
|
|
|
if descs[0].Method != "GET" {
|
|
|
|
|
t.Fatalf("expected descs[0].Method=%q, got %q", "GET", descs[0].Method)
|
|
|
|
|
}
|
|
|
|
|
if descs[0].Summary != "List items" {
|
|
|
|
|
t.Fatalf("expected descs[0].Summary=%q, got %q", "List items", descs[0].Summary)
|
|
|
|
|
}
|
|
|
|
|
if descs[1].Method != "POST" {
|
|
|
|
|
t.Fatalf("expected descs[1].Method=%q, got %q", "POST", descs[1].Method)
|
|
|
|
|
}
|
|
|
|
|
if descs[1].RequestBody == nil {
|
|
|
|
|
t.Fatal("expected descs[1].RequestBody to be non-nil")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDescribableGroup_Good_EmptyDescribe(t *testing.T) {
|
|
|
|
|
stub := &describableStub{
|
|
|
|
|
descriptions: nil,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var dg api.DescribableGroup = stub
|
|
|
|
|
descs := dg.Describe()
|
|
|
|
|
|
|
|
|
|
if descs != nil {
|
|
|
|
|
t.Fatalf("expected nil descriptions, got %v", descs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDescribableGroup_Good_MultipleVerbs(t *testing.T) {
|
|
|
|
|
stub := &describableStub{
|
|
|
|
|
descriptions: []api.RouteDescription{
|
|
|
|
|
{Method: "GET", Path: "/resources", Summary: "List resources"},
|
|
|
|
|
{Method: "POST", Path: "/resources", Summary: "Create resource"},
|
|
|
|
|
{Method: "DELETE", Path: "/resources/:id", Summary: "Delete resource"},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var dg api.DescribableGroup = stub
|
|
|
|
|
descs := dg.Describe()
|
|
|
|
|
|
|
|
|
|
if len(descs) != 3 {
|
|
|
|
|
t.Fatalf("expected 3 descriptions, got %d", len(descs))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expected := []string{"GET", "POST", "DELETE"}
|
|
|
|
|
for i, want := range expected {
|
|
|
|
|
if descs[i].Method != want {
|
|
|
|
|
t.Fatalf("expected descs[%d].Method=%q, got %q", i, want, descs[i].Method)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDescribableGroup_Bad_NilSchemas(t *testing.T) {
|
|
|
|
|
stub := &describableStub{
|
|
|
|
|
descriptions: []api.RouteDescription{
|
|
|
|
|
{
|
|
|
|
|
Method: "GET",
|
|
|
|
|
Path: "/health",
|
|
|
|
|
Summary: "Health check",
|
|
|
|
|
RequestBody: nil,
|
|
|
|
|
Response: nil,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var dg api.DescribableGroup = stub
|
|
|
|
|
descs := dg.Describe()
|
|
|
|
|
|
|
|
|
|
if len(descs) != 1 {
|
|
|
|
|
t.Fatalf("expected 1 description, got %d", len(descs))
|
|
|
|
|
}
|
|
|
|
|
if descs[0].RequestBody != nil {
|
|
|
|
|
t.Fatalf("expected nil RequestBody, got %v", descs[0].RequestBody)
|
|
|
|
|
}
|
|
|
|
|
if descs[0].Response != nil {
|
|
|
|
|
t.Fatalf("expected nil Response, got %v", descs[0].Response)
|
|
|
|
|
}
|
|
|
|
|
}
|