mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
feat(gateway): /g/:slug proxy data plane with key auth and rate limits (#66)
The admin surface under /admin/gateway managed api_services and api_keys, but no public route ever consumed them. Mounts /g/:slug and /g/:slug/* on the root router (outside /api/v1 so route_prefix stays clean) and runs the full pipeline: slug lookup -> X-API-Key/Bearer extraction -> prefix-indexed bcrypt key auth -> allowed_service_ids scope -> per-minute buckets (key and service caps) -> monthly usage_counters quotas -> gateway.ProxyRequest with per-service timeout and upstream auth headers -> usage/metrics/incident recording. Live-verified: proxied GET/POST, 401/404/429 boundaries, counter upserts, UPSTREAM_5XX incidents. Unit tests cover URL building, the rpm bucket, key extraction, and the service allowlist. Generated with [Devin](https://devin.ai) Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
1409186a1f
commit
d5909172ae
5 files changed
+465
-2
No files matched your search
@@ -0,0 +1,64 @@
|
||||
package gateway
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBuildTargetURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
upstream, prefix, path, query, want string
|
||||
}{
|
||||
{
|
||||
name: "strips route prefix",
|
||||
upstream: "https://api.internal:8080",
|
||||
prefix: "/g/weather",
|
||||
path: "/g/weather/v1/current",
|
||||
query: "city=oslo",
|
||||
want: "https://api.internal:8080/v1/current?city=oslo",
|
||||
},
|
||||
{
|
||||
name: "prefix root maps to upstream root",
|
||||
upstream: "https://api.internal",
|
||||
prefix: "/g/weather",
|
||||
path: "/g/weather",
|
||||
want: "https://api.internal/",
|
||||
},
|
||||
{
|
||||
name: "upstream base path is preserved",
|
||||
upstream: "https://api.internal/base",
|
||||
prefix: "/g/x",
|
||||
path: "/g/x/ping",
|
||||
want: "https://api.internal/base/ping",
|
||||
},
|
||||
{
|
||||
name: "root prefix leaves path untouched",
|
||||
upstream: "https://api.internal",
|
||||
prefix: "/",
|
||||
path: "/g/raw",
|
||||
want: "https://api.internal/g/raw",
|
||||
},
|
||||
{
|
||||
name: "path without prefix is still proxied",
|
||||
upstream: "https://api.internal",
|
||||
prefix: "/g/x",
|
||||
path: "/other",
|
||||
want: "https://api.internal/other",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := BuildTargetURL(tc.upstream, tc.prefix, tc.path, tc.query)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Fatalf("got %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTargetURL_InvalidUpstream(t *testing.T) {
|
||||
if _, err := BuildTargetURL("://bad", "/g/x", "/g/x/y", ""); err == nil {
|
||||
t.Fatal("expected error for unparsable upstream")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user