fix(api): snapshot swagger groups

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 00:24:54 +00:00
parent 2c87fa02cb
commit ffbb6d83d0
2 changed files with 98 additions and 18 deletions

View file

@ -11,6 +11,7 @@ import (
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/swaggo/swag"
"slices"
)
// swaggerSeq provides unique instance names so multiple Engine instances
@ -26,6 +27,13 @@ type swaggerSpec struct {
doc string
}
func newSwaggerSpec(builder *SpecBuilder, groups []RouteGroup) *swaggerSpec {
return &swaggerSpec{
builder: builder,
groups: slices.Clone(groups),
}
}
// ReadDoc returns the OpenAPI 3.1 JSON document for this spec.
func (s *swaggerSpec) ReadDoc() string {
s.once.Do(func() {
@ -41,24 +49,21 @@ func (s *swaggerSpec) ReadDoc() string {
// registerSwagger mounts the Swagger UI and doc.json endpoint.
func registerSwagger(g *gin.Engine, title, description, version, graphqlPath, termsOfService, contactName, contactURL, contactEmail string, servers []string, licenseName, licenseURL, externalDocsDescription, externalDocsURL string, groups []RouteGroup) {
spec := &swaggerSpec{
builder: &SpecBuilder{
Title: title,
Description: description,
Version: version,
GraphQLPath: graphqlPath,
TermsOfService: termsOfService,
ContactName: contactName,
ContactURL: contactURL,
ContactEmail: contactEmail,
Servers: servers,
LicenseName: licenseName,
LicenseURL: licenseURL,
ExternalDocsDescription: externalDocsDescription,
ExternalDocsURL: externalDocsURL,
},
groups: groups,
}
spec := newSwaggerSpec(&SpecBuilder{
Title: title,
Description: description,
Version: version,
GraphQLPath: graphqlPath,
TermsOfService: termsOfService,
ContactName: contactName,
ContactURL: contactURL,
ContactEmail: contactEmail,
Servers: servers,
LicenseName: licenseName,
LicenseURL: licenseURL,
ExternalDocsDescription: externalDocsDescription,
ExternalDocsURL: externalDocsURL,
}, groups)
name := fmt.Sprintf("swagger_%d", swaggerSeq.Add(1))
swag.Register(name, spec)
g.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.NewHandler(), ginSwagger.InstanceName(name)))

75
swagger_internal_test.go Normal file
View file

@ -0,0 +1,75 @@
// SPDX-License-Identifier: EUPL-1.2
package api
import (
"encoding/json"
"testing"
"github.com/gin-gonic/gin"
)
type swaggerSnapshotGroup struct {
name string
basePath string
descs []RouteDescription
}
func (g *swaggerSnapshotGroup) Name() string { return g.name }
func (g *swaggerSnapshotGroup) BasePath() string { return g.basePath }
func (g *swaggerSnapshotGroup) RegisterRoutes(_ *gin.RouterGroup) {}
func (g *swaggerSnapshotGroup) Describe() []RouteDescription {
return g.descs
}
func TestSwaggerSpec_ReadDoc_Good_SnapshotsGroups(t *testing.T) {
original := &swaggerSnapshotGroup{
name: "first",
basePath: "/first",
descs: []RouteDescription{
{
Method: "GET",
Path: "/ping",
Summary: "First group",
Response: map[string]any{
"type": "string",
},
},
},
}
replacement := &swaggerSnapshotGroup{
name: "second",
basePath: "/second",
descs: []RouteDescription{
{
Method: "GET",
Path: "/pong",
Summary: "Second group",
Response: map[string]any{
"type": "string",
},
},
},
}
groups := []RouteGroup{original}
spec := newSwaggerSpec(&SpecBuilder{
Title: "Test",
Version: "1.0.0",
}, groups)
groups[0] = replacement
var doc map[string]any
if err := json.Unmarshal([]byte(spec.ReadDoc()), &doc); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
paths := doc["paths"].(map[string]any)
if _, ok := paths["/first/ping"]; !ok {
t.Fatal("expected original group path to remain in the spec")
}
if _, ok := paths["/second/pong"]; ok {
t.Fatal("did not expect mutated group path to leak into the spec")
}
}