diff --git a/swagger.go b/swagger.go index 9c0cb27..3cfc20e 100644 --- a/swagger.go +++ b/swagger.go @@ -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))) diff --git a/swagger_internal_test.go b/swagger_internal_test.go new file mode 100644 index 0000000..70260b7 --- /dev/null +++ b/swagger_internal_test.go @@ -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") + } +}