Files
livekit/pkg/agent/endpoint/template_test.go
T
Théo Monnom 9711e7468c agent endpoints: normalize trailing slashes during routing instead of 307
A slash mismatch (e.g. /hook/ for a route registered as /hook) is now rewritten
to the registered form and served directly, rather than 307-redirected. Webhook
clients often don't follow redirects, and a redirect from the final routing hop
resends the body and pays the whole routing path twice. Exact form is matched
first, so a route registered with a trailing slash is served as-is.
2026-08-30 23:30:00 -07:00

120 lines
3.9 KiB
Go

package endpoint
import (
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
)
func TestTemplateStarletteSemantics(t *testing.T) {
cases := []struct {
template string
path string
match bool
}{
{"/token", "/token", true},
{"/token", "/token/", false},
{"/token", "/Token", false},
{"/users/{id}", "/users/42", true},
{"/users/{id}", "/users/42/posts", false},
{"/users/{id}", "/users/", false},
{"/users/{id:int}", "/users/42", true},
{"/users/{id:int}", "/users/4x2", false},
{"/files/{p:path}", "/files/a/b/c.txt", true},
{"/files/{p:path}", "/files/", true},
{"/price/{v:float}", "/price/1.25", true},
{"/price/{v:float}", "/price/1.", false},
{"/obj/{u:uuid}", "/obj/123e4567-e89b-12d3-a456-426614174000", true},
// starlette's uuid convertor makes every hyphen optional
{"/obj/{u:uuid}", "/obj/123e4567e89b12d3a456426614174000", true},
{"/obj/{u:uuid}", "/obj/123e4567", false},
{"/a/{x}/b/{y}", "/a/1/b/2", true},
{"/a/{x}/b/{y}", "/a/1/c/2", false},
}
for _, c := range cases {
tpl, err := ParseTemplate(c.template)
require.NoError(t, err, c.template)
require.Equal(t, c.match, tpl.Match(c.path), "%s vs %s", c.template, c.path)
}
}
func TestTemplateRejectsCustomConvertors(t *testing.T) {
_, err := ParseTemplate("/x/{id:slug}")
require.Error(t, err)
_, err = ParseTemplate("/x/{a}/{a}")
require.Error(t, err)
_, err = ParseTemplate("no-slash")
require.Error(t, err)
}
func ep(path string, methods []string, public bool) *livekit.AgentHttp_AgentEndpoint {
return &livekit.AgentHttp_AgentEndpoint{Path: path, Methods: methods, Public: public}
}
func TestManifestFullPartialSemantics(t *testing.T) {
// POST /x registered after GET /x must still serve POSTs (starlette scans
// for a FULL match before settling for the PARTIAL 405)
m, err := ParseManifest([]*livekit.AgentHttp_AgentEndpoint{
ep("/x", []string{"GET"}, true),
ep("/x", []string{"POST"}, true),
})
require.NoError(t, err)
r, res := m.Match("/x", http.MethodPost)
require.Equal(t, MatchFull, res)
require.Contains(t, r.Methods, "POST")
// the manifest carries the app's methods verbatim: FastAPI does not imply
// HEAD from GET, so neither does the matcher
_, res = m.Match("/x", http.MethodHead)
require.Equal(t, MatchPartial, res)
// PARTIAL only when no route serves the method
_, res = m.Match("/x", http.MethodDelete)
require.Equal(t, MatchPartial, res)
_, res = m.Match("/nope", http.MethodGet)
require.Equal(t, MatchNone, res)
}
func TestManifestSlashAlternate(t *testing.T) {
m, err := ParseManifest([]*livekit.AgentHttp_AgentEndpoint{
ep("/hook", []string{"POST"}, true),
})
require.NoError(t, err)
// a slash-mismatched request normalizes to the registered form
alt, ok := m.slashAlternate("/hook/", http.MethodPost)
require.True(t, ok)
require.Equal(t, "/hook", alt)
// no route matches either slash form
_, ok = m.slashAlternate("/other/", http.MethodPost)
require.False(t, ok)
}
func TestManifestValidation(t *testing.T) {
_, err := ParseManifest([]*livekit.AgentHttp_AgentEndpoint{ep("/x", nil, false)})
require.Error(t, err, "http endpoint without methods")
_, err = ParseManifest([]*livekit.AgentHttp_AgentEndpoint{ep("/x", []string{"get"}, false)})
require.Error(t, err, "lowercase method")
_, err = ParseManifest([]*livekit.AgentHttp_AgentEndpoint{ep("/x", []string{"GTE"}, false)})
require.Error(t, err, "unsupported method rejected")
// the full FastAPI/starlette verb set is accepted
_, err = ParseManifest([]*livekit.AgentHttp_AgentEndpoint{
ep("/x", []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "TRACE"}, false),
})
require.NoError(t, err, "all FastAPI verbs accepted")
_, err = ParseManifest([]*livekit.AgentHttp_AgentEndpoint{
{Path: "/ws", Kind: livekit.AgentHttp_AEK_TEXT, Methods: []string{"GET"}},
})
require.Error(t, err, "unsupported endpoint kind")
}