Skip to content

Commit 7e9fe0c

Browse files
fix: hidden filter does not hide preview API if a stable is already released (#1091)
1 parent 527b96b commit 7e9fe0c

File tree

2 files changed

+142
-1
lines changed

2 files changed

+142
-1
lines changed

tools/cli/internal/openapi/filter/hidden_envs.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"strings"
2020

2121
"github.com/getkin/kin-openapi/openapi3"
22+
"github.com/mongodb/openapi/tools/cli/internal/apiversion"
2223
)
2324

2425
const (
@@ -132,11 +133,13 @@ func (f *HiddenEnvsFilter) removeItemsIfHiddenForEnv(schema *openapi3.SchemaRef)
132133
}
133134
}
134135

136+
// Remove OpenAPI Response, RequestBody and Operation if they are hidden for the specific environment.
137+
// Note: removeOperationIfHiddenForEnv must run after removeResponseIfHiddenForEnv.
135138
func (f *HiddenEnvsFilter) applyOnPath(pathItem *openapi3.PathItem) error {
136139
for k, operation := range pathItem.Operations() {
137-
f.removeOperationIfHiddenForEnv(k, pathItem, operation)
138140
f.removeResponseIfHiddenForEnv(operation)
139141
f.removeRequestBodyIfHiddenForEnv(operation)
142+
f.removeOperationIfHiddenForEnv(k, pathItem, operation)
140143
}
141144

142145
return nil
@@ -215,6 +218,11 @@ func (f *HiddenEnvsFilter) removeResponseIfHiddenForEnv(operation *openapi3.Oper
215218
}
216219
}
217220

221+
// isOperationHiddenForEnv determines if an operation should be hidden for the target environment.
222+
// It returns true if the operation is explicitly marked as hidden via the extension, or if
223+
// the target version is non-stable (preview/upcoming) and the operation lacks the corresponding
224+
// content type in its 2xx responses. isOperationHiddenForEnv must be executed after the response was already
225+
// filtered out.
218226
func (f *HiddenEnvsFilter) isOperationHiddenForEnv(operation *openapi3.Operation) bool {
219227
if operation == nil {
220228
return false
@@ -225,6 +233,41 @@ func (f *HiddenEnvsFilter) isOperationHiddenForEnv(operation *openapi3.Operation
225233
return isHiddenExtensionEqualToTargetEnv(extension, f.metadata.targetEnv)
226234
}
227235

236+
if f.metadata.targetVersion == nil || f.metadata.targetVersion.IsStable() {
237+
return false
238+
}
239+
240+
// When targeting non-stable versions (preview or upcoming), x-hidden-envs is often applied narrowly
241+
// to specific response content types to preserve the stable version in production.
242+
//
243+
// Since removeResponseIfHiddenForEnv has already executed, any hidden content types are gone.
244+
// We must now ensure the operation still contains content relevant to the target version.
245+
// If the 2xx responses lack the target-specific content type (e.g. "preview"), the operation
246+
// is effectively hidden for this environment and should be removed entirely.
247+
if f.metadata.targetVersion.IsPreview() {
248+
return !hasContentTypeInTheResponse(operation, apiversion.PreviewStabilityLevel)
249+
}
250+
251+
return !hasContentTypeInTheResponse(operation, apiversion.UpcomingStabilityLevel)
252+
}
253+
254+
func hasContentTypeInTheResponse(operation *openapi3.Operation, contentType string) bool {
255+
for responseCode, response := range operation.Responses.Map() {
256+
if !strings.HasPrefix(responseCode, "20") {
257+
continue
258+
}
259+
260+
if response.Value == nil || response.Value.Content == nil {
261+
continue
262+
}
263+
264+
for contentKey := range response.Value.Content {
265+
if strings.Contains(contentKey, contentType) {
266+
return true
267+
}
268+
}
269+
}
270+
228271
return false
229272
}
230273

tools/cli/internal/openapi/filter/hidden_envs_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,18 @@ import (
1919
"testing"
2020

2121
"github.com/getkin/kin-openapi/openapi3"
22+
"github.com/mongodb/openapi/tools/cli/internal/apiversion"
2223
"github.com/mongodb/openapi/tools/cli/internal/pointer"
2324
"github.com/stretchr/testify/assert"
2425
"github.com/stretchr/testify/require"
2526
)
2627

2728
func TestIsOperationHiddenForEnv(t *testing.T) {
29+
previewVersion, err := apiversion.New(apiversion.WithVersion(apiversion.PreviewStabilityLevel))
30+
require.NoError(t, err)
31+
upcomingVersion, err := apiversion.New(apiversion.WithVersion("2024-08-05.upcoming"))
32+
require.NoError(t, err)
33+
2834
tests := []struct {
2935
name string
3036
operation *openapi3.Operation
@@ -101,6 +107,98 @@ func TestIsOperationHiddenForEnv(t *testing.T) {
101107
},
102108
wantHidden: false,
103109
},
110+
{
111+
name: "Operation with targetVersion = preview and stable response content",
112+
operation: &openapi3.Operation{
113+
Extensions: map[string]any{},
114+
Responses: openapi3.NewResponses(openapi3.WithName("200", &openapi3.Response{
115+
Description: pointer.Get("Success"),
116+
Content: map[string]*openapi3.MediaType{
117+
"application/vnd.atlas.2024-08-05+json": {
118+
Schema: &openapi3.SchemaRef{
119+
Extensions: map[string]any{
120+
"envs": "prod",
121+
},
122+
},
123+
},
124+
},
125+
})),
126+
},
127+
metadata: &Metadata{
128+
targetEnv: "prod",
129+
targetVersion: previewVersion,
130+
},
131+
wantHidden: true,
132+
},
133+
{
134+
name: "Operation with targetVersion = preview and preview response content",
135+
operation: &openapi3.Operation{
136+
Extensions: map[string]any{},
137+
Responses: openapi3.NewResponses(openapi3.WithName("200", &openapi3.Response{
138+
Description: pointer.Get("Success"),
139+
Content: map[string]*openapi3.MediaType{
140+
"application/vnd.atlas.preview+json": {
141+
Schema: &openapi3.SchemaRef{
142+
Extensions: map[string]any{
143+
"envs": "dev",
144+
},
145+
},
146+
},
147+
},
148+
})),
149+
},
150+
metadata: &Metadata{
151+
targetEnv: "prod",
152+
targetVersion: previewVersion,
153+
},
154+
wantHidden: false,
155+
},
156+
{
157+
name: "Operation with targetVersion = upcoming and stable response content",
158+
operation: &openapi3.Operation{
159+
Extensions: map[string]any{},
160+
Responses: openapi3.NewResponses(openapi3.WithName("200", &openapi3.Response{
161+
Description: pointer.Get("Success"),
162+
Content: map[string]*openapi3.MediaType{
163+
"application/vnd.atlas.2024-08-05+json": {
164+
Schema: &openapi3.SchemaRef{
165+
Extensions: map[string]any{
166+
"envs": "prod",
167+
},
168+
},
169+
},
170+
},
171+
})),
172+
},
173+
metadata: &Metadata{
174+
targetEnv: "prod",
175+
targetVersion: upcomingVersion,
176+
},
177+
wantHidden: true,
178+
},
179+
{
180+
name: "Operation with targetVersion = upcoming and upcoming response content",
181+
operation: &openapi3.Operation{
182+
Extensions: map[string]any{},
183+
Responses: openapi3.NewResponses(openapi3.WithName("200", &openapi3.Response{
184+
Description: pointer.Get("Success"),
185+
Content: map[string]*openapi3.MediaType{
186+
"application/vnd.atlas.2024-08-05.upcoming+json": {
187+
Schema: &openapi3.SchemaRef{
188+
Extensions: map[string]any{
189+
"envs": "dev",
190+
},
191+
},
192+
},
193+
},
194+
})),
195+
},
196+
metadata: &Metadata{
197+
targetEnv: "prod",
198+
targetVersion: upcomingVersion,
199+
},
200+
wantHidden: false,
201+
},
104202
}
105203

106204
for _, tt := range tests {

0 commit comments

Comments
 (0)