-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhidden_path_test.go
More file actions
130 lines (110 loc) · 4.74 KB
/
Copy pathhidden_path_test.go
File metadata and controls
130 lines (110 loc) · 4.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package fiberoapi
import (
"encoding/json"
"io"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type hiddenInput struct {
ID string `uri:"id" validate:"required"`
}
type hiddenOutput struct {
Message string `json:"message"`
}
// HiddenOnlyType is referenced ONLY by the hidden route in the leak test.
// We deliberately give it a distinctive name so we can grep for it in the
// generated spec.
type HiddenOnlyType struct {
Secret string `json:"secret"`
}
func TestHidden_RouteServesTrafficButAbsentFromSpec(t *testing.T) {
app := fiber.New()
oapi := New(app)
Get(oapi, "/admin/debug/:id", func(c fiber.Ctx, in hiddenInput) (hiddenOutput, struct{}) {
return hiddenOutput{Message: "debug " + in.ID}, struct{}{}
}, OpenAPIOptions{
OperationID: "internalDebug",
Hidden: true,
})
Get(oapi, "/public/:id", func(c fiber.Ctx, in hiddenInput) (hiddenOutput, struct{}) {
return hiddenOutput{Message: "public " + in.ID}, struct{}{}
}, OpenAPIOptions{OperationID: "public"})
// Runtime: both routes serve traffic.
respHidden, err := app.Test(httptest.NewRequest("GET", "/admin/debug/abc", nil))
require.NoError(t, err)
defer respHidden.Body.Close()
require.Equal(t, 200, respHidden.StatusCode)
body, err := io.ReadAll(respHidden.Body)
require.NoError(t, err)
var got hiddenOutput
require.NoError(t, json.Unmarshal(body, &got))
assert.Equal(t, "debug abc", got.Message)
respPublic, err := app.Test(httptest.NewRequest("GET", "/public/xyz", nil))
require.NoError(t, err)
defer respPublic.Body.Close()
require.Equal(t, 200, respPublic.StatusCode)
// Spec: only the public path appears.
spec := oapi.GenerateOpenAPISpec()
paths := spec["paths"].(map[string]any)
_, hasPublic := paths["/public/{id}"]
_, hasHidden := paths["/admin/debug/{id}"]
assert.True(t, hasPublic, "public route must appear in the spec")
assert.False(t, hasHidden, "hidden route must NOT appear in the spec")
}
func TestHidden_TypesOnlyUsedByHiddenRouteDoNotLeak(t *testing.T) {
app := fiber.New()
oapi := New(app)
// A type that only the hidden route references — it must not surface
// under components.schemas. Otherwise an attacker reading the spec could
// guess the route's shape even without the path entry.
Get(oapi, "/admin/secret", func(c fiber.Ctx, _ struct{}) (HiddenOnlyType, struct{}) {
return HiddenOnlyType{Secret: "shh"}, struct{}{}
}, OpenAPIOptions{
OperationID: "adminSecret",
Hidden: true,
})
// Visible route using a distinct type, so we can confirm schema gen still works.
Get(oapi, "/public", func(c fiber.Ctx, _ struct{}) (hiddenOutput, struct{}) {
return hiddenOutput{Message: "ok"}, struct{}{}
}, OpenAPIOptions{OperationID: "publicProbe"})
spec := oapi.GenerateOpenAPISpec()
schemas := spec["components"].(map[string]any)["schemas"].(map[string]any)
_, hasHiddenType := schemas["HiddenOnlyType"]
_, hasVisibleType := schemas["hiddenOutput"]
assert.False(t, hasHiddenType, "type only used by a hidden route must not appear in components.schemas")
assert.True(t, hasVisibleType, "type used by a visible route should still appear")
}
func TestHidden_AbsentOptionKeepsRouteInSpec(t *testing.T) {
// Regression: by default (Hidden zero value = false), every route surfaces.
app := fiber.New()
oapi := New(app)
Get(oapi, "/public/:id", func(c fiber.Ctx, in hiddenInput) (hiddenOutput, struct{}) {
return hiddenOutput{Message: "ok"}, struct{}{}
}, OpenAPIOptions{OperationID: "public"})
spec := oapi.GenerateOpenAPISpec()
paths := spec["paths"].(map[string]any)
_, hasPublic := paths["/public/{id}"]
assert.True(t, hasPublic, "route without Hidden must appear in the spec")
}
func TestHidden_TypeSharedBetweenHiddenAndVisibleStillSurfaces(t *testing.T) {
// Edge case: when a type is used by BOTH a hidden and a visible route,
// it must remain in components.schemas because the visible route still
// needs to $ref it. The Hidden skip only suppresses contributions from
// hidden routes — it does not retroactively remove a type that another
// visible route depends on.
app := fiber.New()
oapi := New(app)
Get(oapi, "/admin/secret/:id", func(c fiber.Ctx, in hiddenInput) (hiddenOutput, struct{}) {
return hiddenOutput{Message: "secret " + in.ID}, struct{}{}
}, OpenAPIOptions{OperationID: "secret", Hidden: true})
Get(oapi, "/public/:id", func(c fiber.Ctx, in hiddenInput) (hiddenOutput, struct{}) {
return hiddenOutput{Message: "public " + in.ID}, struct{}{}
}, OpenAPIOptions{OperationID: "public"})
spec := oapi.GenerateOpenAPISpec()
schemas := spec["components"].(map[string]any)["schemas"].(map[string]any)
_, hasShared := schemas["hiddenInput"]
assert.True(t, hasShared, "type shared with a visible route must remain in components.schemas")
}