fix(openapi): normalise spec builder metadata
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
2b71c78c33
commit
0022931eff
2 changed files with 119 additions and 14 deletions
61
openapi.go
61
openapi.go
|
|
@ -81,20 +81,25 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) {
|
|||
if sb == nil {
|
||||
sb = &SpecBuilder{}
|
||||
}
|
||||
sb = sb.snapshot()
|
||||
|
||||
prepared := prepareRouteGroups(groups)
|
||||
|
||||
info := map[string]any{
|
||||
"title": sb.Title,
|
||||
"description": sb.Description,
|
||||
"version": sb.Version,
|
||||
}
|
||||
if sb.Summary != "" {
|
||||
info["summary"] = sb.Summary
|
||||
}
|
||||
|
||||
spec := map[string]any{
|
||||
"openapi": "3.1.0",
|
||||
"jsonSchemaDialect": openAPIDialect,
|
||||
"info": map[string]any{
|
||||
"title": sb.Title,
|
||||
"summary": sb.Summary,
|
||||
"description": sb.Description,
|
||||
"version": sb.Version,
|
||||
},
|
||||
"paths": sb.buildPaths(prepared),
|
||||
"tags": sb.buildTags(prepared),
|
||||
"info": info,
|
||||
"paths": sb.buildPaths(prepared),
|
||||
"tags": sb.buildTags(prepared),
|
||||
}
|
||||
|
||||
if sb.LicenseName != "" {
|
||||
|
|
@ -106,12 +111,6 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) {
|
|||
}
|
||||
spec["info"].(map[string]any)["license"] = license
|
||||
}
|
||||
if sb.Summary != "" {
|
||||
spec["info"].(map[string]any)["summary"] = sb.Summary
|
||||
} else {
|
||||
delete(spec["info"].(map[string]any), "summary")
|
||||
}
|
||||
|
||||
if swaggerPath := sb.effectiveSwaggerPath(); swaggerPath != "" {
|
||||
spec["x-swagger-ui-path"] = normaliseSwaggerPath(swaggerPath)
|
||||
}
|
||||
|
|
@ -1966,6 +1965,40 @@ func (sb *SpecBuilder) effectiveAuthentikPublicPaths() []string {
|
|||
return normalisePublicPaths(paths)
|
||||
}
|
||||
|
||||
// snapshot returns a trimmed copy of the builder so Build operates on stable
|
||||
// input even when callers reuse or mutate their original configuration.
|
||||
func (sb *SpecBuilder) snapshot() *SpecBuilder {
|
||||
if sb == nil {
|
||||
return &SpecBuilder{}
|
||||
}
|
||||
|
||||
out := *sb
|
||||
out.Title = strings.TrimSpace(out.Title)
|
||||
out.Summary = strings.TrimSpace(out.Summary)
|
||||
out.Description = strings.TrimSpace(out.Description)
|
||||
out.Version = strings.TrimSpace(out.Version)
|
||||
out.SwaggerPath = strings.TrimSpace(out.SwaggerPath)
|
||||
out.GraphQLPath = strings.TrimSpace(out.GraphQLPath)
|
||||
out.WSPath = strings.TrimSpace(out.WSPath)
|
||||
out.SSEPath = strings.TrimSpace(out.SSEPath)
|
||||
out.TermsOfService = strings.TrimSpace(out.TermsOfService)
|
||||
out.ContactName = strings.TrimSpace(out.ContactName)
|
||||
out.ContactURL = strings.TrimSpace(out.ContactURL)
|
||||
out.ContactEmail = strings.TrimSpace(out.ContactEmail)
|
||||
out.LicenseName = strings.TrimSpace(out.LicenseName)
|
||||
out.LicenseURL = strings.TrimSpace(out.LicenseURL)
|
||||
out.ExternalDocsDescription = strings.TrimSpace(out.ExternalDocsDescription)
|
||||
out.ExternalDocsURL = strings.TrimSpace(out.ExternalDocsURL)
|
||||
out.CacheTTL = strings.TrimSpace(out.CacheTTL)
|
||||
out.I18nDefaultLocale = strings.TrimSpace(out.I18nDefaultLocale)
|
||||
out.Servers = slices.Clone(sb.Servers)
|
||||
out.I18nSupportedLocales = slices.Clone(sb.I18nSupportedLocales)
|
||||
out.AuthentikPublicPaths = normalisePublicPaths(sb.AuthentikPublicPaths)
|
||||
out.SecuritySchemes = cloneSecuritySchemes(sb.SecuritySchemes)
|
||||
|
||||
return &out
|
||||
}
|
||||
|
||||
// isPublicOperationPath reports whether an OpenAPI path should be documented
|
||||
// as public because Authentik bypasses it in the running engine.
|
||||
func (sb *SpecBuilder) isPublicOperationPath(path string) bool {
|
||||
|
|
|
|||
|
|
@ -366,6 +366,78 @@ func TestSpecBuilder_Good_CommonResponseComponentsArePublished(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_NormalisesMetadataAtBuild(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: " Test API ",
|
||||
Summary: " ",
|
||||
Description: " Trimmed description ",
|
||||
Version: " 1.2.3 ",
|
||||
TermsOfService: " https://example.com/terms ",
|
||||
ContactName: " API Support ",
|
||||
ContactURL: " https://example.com/support ",
|
||||
ContactEmail: " support@example.com ",
|
||||
LicenseName: " EUPL-1.2 ",
|
||||
LicenseURL: " https://eupl.eu/1.2/en/ ",
|
||||
ExternalDocsURL: " https://example.com/docs ",
|
||||
ExternalDocsDescription: " Developer guide ",
|
||||
SwaggerPath: " /docs/ ",
|
||||
}
|
||||
|
||||
data, err := sb.Build(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var spec map[string]any
|
||||
if err := json.Unmarshal(data, &spec); err != nil {
|
||||
t.Fatalf("invalid JSON: %v", err)
|
||||
}
|
||||
|
||||
info := spec["info"].(map[string]any)
|
||||
if info["title"] != "Test API" {
|
||||
t.Fatalf("expected trimmed title, got %v", info["title"])
|
||||
}
|
||||
if _, ok := info["summary"]; ok {
|
||||
t.Fatal("expected blank summary to be omitted")
|
||||
}
|
||||
if info["description"] != "Trimmed description" {
|
||||
t.Fatalf("expected trimmed description, got %v", info["description"])
|
||||
}
|
||||
if info["version"] != "1.2.3" {
|
||||
t.Fatalf("expected trimmed version, got %v", info["version"])
|
||||
}
|
||||
if info["termsOfService"] != "https://example.com/terms" {
|
||||
t.Fatalf("expected trimmed termsOfService, got %v", info["termsOfService"])
|
||||
}
|
||||
contact := info["contact"].(map[string]any)
|
||||
if contact["name"] != "API Support" {
|
||||
t.Fatalf("expected trimmed contact name, got %v", contact["name"])
|
||||
}
|
||||
if contact["url"] != "https://example.com/support" {
|
||||
t.Fatalf("expected trimmed contact url, got %v", contact["url"])
|
||||
}
|
||||
if contact["email"] != "support@example.com" {
|
||||
t.Fatalf("expected trimmed contact email, got %v", contact["email"])
|
||||
}
|
||||
license := info["license"].(map[string]any)
|
||||
if license["name"] != "EUPL-1.2" {
|
||||
t.Fatalf("expected trimmed licence name, got %v", license["name"])
|
||||
}
|
||||
if license["url"] != "https://eupl.eu/1.2/en/" {
|
||||
t.Fatalf("expected trimmed licence url, got %v", license["url"])
|
||||
}
|
||||
externalDocs := spec["externalDocs"].(map[string]any)
|
||||
if externalDocs["description"] != "Developer guide" {
|
||||
t.Fatalf("expected trimmed external docs description, got %v", externalDocs["description"])
|
||||
}
|
||||
if externalDocs["url"] != "https://example.com/docs" {
|
||||
t.Fatalf("expected trimmed external docs url, got %v", externalDocs["url"])
|
||||
}
|
||||
if got := spec["x-swagger-ui-path"]; got != "/docs" {
|
||||
t.Fatalf("expected trimmed swagger path, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_SwaggerUIPathExtension(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue