Skip to content

Commit f2cd7bb

Browse files
committed
test(server): hold the route table to the claims it makes
A required argument makes a reason impossible to omit. Only an inventory of the claims makes an empty one impossible to merge, so this reads back what the route table declared while it was built and refuses a claim that says nothing. A second test proves that check bites rather than passing because it looks at nothing. The exempt set names the routes that are not resource operations and so keep a direct registration. Echo does not expose a route's handler, so no test can prove that a route outside the set went through a wrapper; what the set buys is the other direction, where joining it is a visible edit next to the reason each member is exempt. Each member is cross-checked against the router, so an entry that outlives its route fails. The three converted routes are pinned to both their shape and their address, which rules out a claim recorded by a wrapper nothing mounted. Fixes: #6940
1 parent eb2f219 commit f2cd7bb

1 file changed

Lines changed: 136 additions & 0 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package routes
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"testing"
7+
8+
"github.com/shellhub-io/shellhub/server/api/pkg/gateway"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// unstatedClaims names every declaration that takes an exception to the default rules without
14+
// saying why. A reason is a required argument, so it cannot be omitted — but it can be left empty,
15+
// and this is what refuses that.
16+
func unstatedClaims(declarations []gateway.Declaration) []string {
17+
unstated := make([]string, 0)
18+
19+
for _, declaration := range declarations {
20+
if declaration.Unbounded && strings.TrimSpace(declaration.UnboundedReason) == "" {
21+
unstated = append(unstated, fmt.Sprintf("%s reads across namespaces and states no reason", declaration.Handler))
22+
}
23+
24+
if declaration.Anonymous && strings.TrimSpace(declaration.AnonymousReason) == "" {
25+
unstated = append(unstated, fmt.Sprintf("%s requires no actor and states no reason", declaration.Handler))
26+
}
27+
}
28+
29+
return unstated
30+
}
31+
32+
// TestRouteTableStatesEveryClaim reads the claims the route table made while it was built. A route
33+
// that reads across namespaces, or that needs no actor, has to say why — otherwise breadth and
34+
// anonymity arrive by omission, which is what the two claims exist to prevent.
35+
func TestRouteTableStatesEveryClaim(t *testing.T) {
36+
authenticatedRouter(t)
37+
38+
declarations := gateway.Declarations()
39+
require.NotEmpty(t, declarations, "the route table registered no wrapped route")
40+
41+
assert.Empty(t, unstatedClaims(declarations))
42+
}
43+
44+
// TestUnstatedClaimsRefusesAnEmptyReason proves the check above bites, rather than passing because
45+
// it looks at nothing.
46+
func TestUnstatedClaimsRefusesAnEmptyReason(t *testing.T) {
47+
unstated := unstatedClaims([]gateway.Declaration{
48+
{Handler: "silent", Unbounded: true, Anonymous: true},
49+
{Handler: "stated", Unbounded: true, UnboundedReason: "because"},
50+
})
51+
52+
require.Len(t, unstated, 2)
53+
for _, complaint := range unstated {
54+
assert.Contains(t, complaint, "silent")
55+
}
56+
}
57+
58+
// wrapperExemptRoutes is the set of routes that are not API resource operations and so keep a
59+
// direct registration instead of one of the gateway shapes.
60+
//
61+
// The install script serves a shell script rather than JSON. The local user authentication
62+
// exchange derives three non-200 outcomes from values that are not errors, and sets two headers
63+
// the console reads. The tag creation handler returns its identifier in a response header, and
64+
// leaves this set once that identifier moves into the response body.
65+
//
66+
// Echo does not expose a route's handler, so no test can prove that a route outside this set went
67+
// through a wrapper. What the set buys is the other direction: joining it is a visible edit here,
68+
// where a reviewer reads why each member is exempt.
69+
var wrapperExemptRoutes = []string{
70+
"GET /api/install",
71+
"POST /api/login",
72+
"POST /api/auth/user",
73+
"POST /api/tags",
74+
"POST /api/namespaces/:tenant/tags",
75+
}
76+
77+
// TestWrapperExemptRoutesAreRegistered catches a stale member: an exempt route that no longer
78+
// exists, or was renamed, leaves the set claiming an exemption for nothing.
79+
func TestWrapperExemptRoutesAreRegistered(t *testing.T) {
80+
router, _, _ := authenticatedRouter(t)
81+
82+
registered := make(map[string]struct{})
83+
for _, route := range router.Router().Routes() {
84+
registered[route.Method+" "+route.Path] = struct{}{}
85+
}
86+
87+
for _, exempt := range wrapperExemptRoutes {
88+
assert.Contains(t, registered, exempt, "the exempt set names %q but no such route is registered", exempt)
89+
}
90+
}
91+
92+
// methodName reduces the fully qualified name the runtime reports for a method value to the method
93+
// itself, so a test can name a handler the way the route table does.
94+
func methodName(qualified string) string {
95+
return strings.TrimSuffix(qualified[strings.LastIndex(qualified, ".")+1:], "-fm")
96+
}
97+
98+
// TestConvertedRoutesDeclareTheirShape pins the three routes this change converted: each answers
99+
// with the shape it was registered under, and each is mounted at the address it claims.
100+
//
101+
// The second half is what keeps the declaration honest. Echo does not expose a route's handler, so
102+
// a declaration cannot be matched to its route in general — but for a named handler at a known
103+
// address, asserting both is enough to rule out a claim recorded by a wrapper nothing mounted.
104+
func TestConvertedRoutesDeclareTheirShape(t *testing.T) {
105+
router, _, _ := authenticatedRouter(t)
106+
107+
registered := make(map[string]struct{})
108+
for _, route := range router.Router().Routes() {
109+
registered[route.Method+" "+route.Path] = struct{}{}
110+
}
111+
112+
shapes := make(map[string]gateway.Shape)
113+
for _, declaration := range gateway.Declarations() {
114+
shapes[methodName(declaration.Handler)] = declaration.Shape
115+
}
116+
117+
converted := []struct {
118+
handler string
119+
shape gateway.Shape
120+
route string
121+
}{
122+
{handler: "EvaluateHealth", shape: gateway.ShapeNone, route: "GET /api" + HealthCheckURL},
123+
{handler: "GetDevice", shape: gateway.ShapeOne, route: "GET /api" + GetDeviceURL},
124+
{handler: "GetDeviceList", shape: gateway.ShapeList, route: "GET /api" + GetDeviceListURL},
125+
}
126+
127+
for _, tc := range converted {
128+
t.Run(tc.handler, func(tt *testing.T) {
129+
declared, found := shapes[tc.handler]
130+
131+
require.True(tt, found, "%s is not registered through a gateway shape", tc.handler)
132+
assert.Equal(tt, tc.shape, declared, "%s answers with the wrong shape", tc.handler)
133+
assert.Contains(tt, registered, tc.route, "%s declares a shape but is not mounted", tc.handler)
134+
})
135+
}
136+
}

0 commit comments

Comments
 (0)