Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion go/apps/api/openapi/openapi-generated.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1762,7 +1762,6 @@ components:
type: string
minLength: 1
maxLength: 255
pattern: "^[a-zA-Z][a-zA-Z0-9_./-]*$"
description: The id or name of the namespace.
example: sms.sign_up
cost:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ properties:
namespace:
type: string
minLength: 1
maxLength: 255 # Reasonable upper bound for namespace identifiers
pattern: "^[a-zA-Z][a-zA-Z0-9_./-]*$"
maxLength: 255
description: The id or name of the namespace.
example: sms.sign_up
cost:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
additionalProperties: false
properties:
namespace:
description: The id or name of the rate limit namespace to list overrides
for.
description: The id or name of the rate limit namespace to list overrides for.
type: string
minLength: 1
maxLength: 255
Expand Down
82 changes: 81 additions & 1 deletion go/apps/api/routes/v2_ratelimit_limit/200_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ func TestLimitSuccessfully(t *testing.T) {

// Test basic rate limiting
t.Run("basic rate limiting", func(t *testing.T) {

namespaceID, namespaceName := createNamespace(t, h)
rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, fmt.Sprintf("ratelimit.%s.limit", namespaceID))

Expand Down Expand Up @@ -256,6 +255,87 @@ func TestLimitSuccessfully(t *testing.T) {
require.Equal(t, int64(1), res2.Body.Data.Limit)
require.Equal(t, int64(0), res2.Body.Data.Remaining)
})
// Test namespace accepts any characters within length bounds
t.Run("namespace accepts any characters within length bounds", func(t *testing.T) {
// Test various character types in namespace names
testCases := []struct {
name string
namespaceName string
}{
{
name: "special characters",
namespaceName: "!@#$%^&*()_+-=[]{}|;':\",./<>?",
},
{
name: "unicode characters",
namespaceName: "αβγδε_测试_테스트_🚀🎉",
},
{
name: "mixed alphanumeric and special",
namespaceName: "test-123_ABC.xyz@domain",
},
{
name: "spaces and tabs",
namespaceName: "namespace with spaces and tabs",
},
{
name: "control characters",
namespaceName: "test\nwith\rnewlines\tand\btabs",
},
{
name: "colon and slash delimiters",
namespaceName: "api:v1:calls/outbound",
},
{
name: "leading and trailing whitespace",
namespaceName: " leading and trailing ",
},
{
name: "minimum length (1 char)",
namespaceName: "a",
},
{
name: "maximum length (255 chars)",
namespaceName: strings.Repeat("x", 255),
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create namespace with the test name
namespaceID := uid.New(uid.RatelimitNamespacePrefix)
err := db.Query.InsertRatelimitNamespace(t.Context(), h.DB.RW(), db.InsertRatelimitNamespaceParams{
ID: namespaceID,
WorkspaceID: h.Resources().UserWorkspace.ID,
Name: tc.namespaceName,
CreatedAt: time.Now().UnixMilli(),
})
require.NoError(t, err)

// Create root key for this namespace
rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, fmt.Sprintf("ratelimit.%s.limit", namespaceID))

headers := http.Header{
"Content-Type": {"application/json"},
"Authorization": {fmt.Sprintf("Bearer %s", rootKey)},
}

req := handler.Request{
Namespace: tc.namespaceName,
Identifier: uid.New("test"),
Limit: 100,
Duration: 60000,
}

// Should be able to use the namespace with any characters
res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req)
require.Equal(t, 200, res.Status, "expected 200 for namespace: %s", tc.namespaceName)
require.NotNil(t, res.Body)
require.True(t, res.Body.Data.Success, "Rate limit should succeed for namespace: %s", tc.namespaceName)
})
}
})

t.Run("rate limiting with active override", func(t *testing.T) {
namespaceID, namespaceName := createNamespace(t, h)
rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, fmt.Sprintf("ratelimit.%s.limit", namespaceID))
Expand Down
56 changes: 28 additions & 28 deletions go/apps/api/routes/v2_ratelimit_limit/400_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,34 +70,34 @@ func TestBadRequests(t *testing.T) {
require.Greater(t, len(res.Body.Error.Errors), 0)
})

// Uncomment and adapt these tests if needed
/*
t.Run("missing namespace", func(t *testing.T) {
req := openapi.V2RatelimitLimitRequestBody{
Identifier: "user_123",
Limit: 100,
Duration: 60000,
}

headers := http.Header{
"Content-Type": {"application/json"},
"Authorization": {fmt.Sprintf("Bearer %s", h.CreateRootKey(h.Resources().UserWorkspace.ID, "ratelimit.*.limit"))},
}

res := testutil.CallRoute[handler.Request, openapi.BadRequestError](h, route, headers, req)

require.Equal(t, 400, res.Status, "expected 400, sent: %+v, received: %s", req, res.RawBody)
require.NotNil(t, res.Body)

require.Equal(t, "https://unkey.com/docs/errors/unkey/application/invalid_input", res.Body.Type)
require.Equal(t, "POST request body for '/v2/ratelimit.limit' failed to validate schema", res.Body.Detail)
require.Equal(t, http.StatusBadRequest, res.Body.Status)
require.Equal(t, "Bad Request", res.Body.Title)
require.NotEmpty(t, res.Body.RequestId)
require.Greater(t, len(res.Body.Errors), 0)
require.Nil(t, res.Body.Instance)
})
*/
t.Run("missing namespace in request", func(t *testing.T) {
// Create a root key with wildcard permission for any namespace
rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "ratelimit.*.limit")

headers := http.Header{
"Content-Type": {"application/json"},
"Authorization": {fmt.Sprintf("Bearer %s", rootKey)},
}

// Request with empty namespace
req := handler.Request{
// namespace missing
Identifier: "user_123",
Limit: 100,
Duration: 60000,
}

// Should return an error for missing namespace
res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req)
require.Equal(t, http.StatusBadRequest, res.Status, "expected 400, sent: %+v, received body: %s", req, res.RawBody)
require.NotNil(t, res.Body)
require.Equal(t, "https://unkey.com/docs/errors/unkey/application/invalid_input", res.Body.Error.Type)
require.Equal(t, "POST request body for '/v2/ratelimit.limit' failed to validate schema", res.Body.Error.Detail)
require.Equal(t, http.StatusBadRequest, res.Body.Error.Status)
require.Equal(t, "Bad Request", res.Body.Error.Title)
require.NotEmpty(t, res.Body.Meta.RequestId)
require.Greater(t, len(res.Body.Error.Errors), 0)
})
}

func TestMissingAuthorizationHeader(t *testing.T) {
Expand Down