mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
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>
82 lines
2.3 KiB
Go
82 lines
2.3 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func TestGatewayRateAllow(t *testing.T) {
|
|
now := time.Date(2026, 10, 15, 12, 0, 30, 0, time.UTC)
|
|
|
|
for i := 0; i < 3; i++ {
|
|
if !gatewayRateAllow("test:a", 3, now) {
|
|
t.Fatalf("call %d within limit rejected", i+1)
|
|
}
|
|
}
|
|
if gatewayRateAllow("test:a", 3, now) {
|
|
t.Fatal("call beyond limit allowed")
|
|
}
|
|
if !gatewayRateAllow("test:a", 3, now.Add(time.Minute)) {
|
|
t.Fatal("new minute window still rejected")
|
|
}
|
|
if !gatewayRateAllow("test:unlimited", 0, now) {
|
|
t.Fatal("non-positive limit should disable the check")
|
|
}
|
|
}
|
|
|
|
func TestExtractGatewayKey(t *testing.T) {
|
|
mkctx := func(headers map[string]string) *gin.Context {
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
c.Request = httptest.NewRequest("GET", "/g/x", nil)
|
|
for k, v := range headers {
|
|
c.Request.Header.Set(k, v)
|
|
}
|
|
return c
|
|
}
|
|
|
|
if got := extractGatewayKey(mkctx(map[string]string{"X-API-Key": "ap_abc"})); got != "ap_abc" {
|
|
t.Fatalf("X-API-Key not extracted: %q", got)
|
|
}
|
|
if got := extractGatewayKey(mkctx(map[string]string{"Authorization": "Bearer ap_def"})); got != "ap_def" {
|
|
t.Fatalf("Bearer not extracted: %q", got)
|
|
}
|
|
// X-API-Key wins when both are present.
|
|
if got := extractGatewayKey(mkctx(map[string]string{
|
|
"X-API-Key": "ap_first",
|
|
"Authorization": "Bearer ap_second",
|
|
})); got != "ap_first" {
|
|
t.Fatalf("X-API-Key should take precedence: %q", got)
|
|
}
|
|
if got := extractGatewayKey(mkctx(map[string]string{})); got != "" {
|
|
t.Fatalf("expected empty key, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestKeyAllowsService(t *testing.T) {
|
|
svcID := "11111111-2222-3333-4444-555555555555"
|
|
other := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
|
|
cases := []struct {
|
|
name string
|
|
allowed string
|
|
want bool
|
|
}{
|
|
{"empty list permits all", "[]", true},
|
|
{"blank permits all", "", true},
|
|
{"invalid json permits all", "not-json", true},
|
|
{"listed service allowed", `["` + svcID + `"]`, true},
|
|
{"unlisted service denied", `["` + other + `"]`, false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
k := &gatewayKey{AllowedServiceIDs: tc.allowed}
|
|
if got := keyAllowsService(k, svcID); got != tc.want {
|
|
t.Fatalf("keyAllowsService(%q) = %v, want %v", tc.allowed, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|