diff --git a/go/apps/api/openapi/openapi-generated.yaml b/go/apps/api/openapi/openapi-generated.yaml index d46a22274c..15744238e5 100644 --- a/go/apps/api/openapi/openapi-generated.yaml +++ b/go/apps/api/openapi/openapi-generated.yaml @@ -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: diff --git a/go/apps/api/openapi/spec/paths/v2/ratelimit/limit/V2RatelimitLimitRequestBody.yaml b/go/apps/api/openapi/spec/paths/v2/ratelimit/limit/V2RatelimitLimitRequestBody.yaml index ae3b49ac6d..85e53c0baa 100644 --- a/go/apps/api/openapi/spec/paths/v2/ratelimit/limit/V2RatelimitLimitRequestBody.yaml +++ b/go/apps/api/openapi/spec/paths/v2/ratelimit/limit/V2RatelimitLimitRequestBody.yaml @@ -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: diff --git a/go/apps/api/openapi/spec/paths/v2/ratelimit/listOverrides/V2RatelimitListOverridesRequestBody.yaml b/go/apps/api/openapi/spec/paths/v2/ratelimit/listOverrides/V2RatelimitListOverridesRequestBody.yaml index 353e745e9b..65909d6570 100644 --- a/go/apps/api/openapi/spec/paths/v2/ratelimit/listOverrides/V2RatelimitListOverridesRequestBody.yaml +++ b/go/apps/api/openapi/spec/paths/v2/ratelimit/listOverrides/V2RatelimitListOverridesRequestBody.yaml @@ -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 diff --git a/go/apps/api/routes/v2_ratelimit_limit/200_test.go b/go/apps/api/routes/v2_ratelimit_limit/200_test.go index 4e14cb9c9a..e97ca836fa 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/200_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/200_test.go @@ -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)) @@ -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)) diff --git a/go/apps/api/routes/v2_ratelimit_limit/400_test.go b/go/apps/api/routes/v2_ratelimit_limit/400_test.go index eea5df7775..195961917f 100644 --- a/go/apps/api/routes/v2_ratelimit_limit/400_test.go +++ b/go/apps/api/routes/v2_ratelimit_limit/400_test.go @@ -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) {