From e8e0842be3d7544661bcbb7ec846ecf8a6299f1c Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Tue, 21 Apr 2026 07:59:35 -0600 Subject: [PATCH] fix(studio): validate auth conn percentage input between 1 and 100 (#45061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Closes [FE-3039](https://linear.app/supabase/issue/FE-3039/validate-conn-percentage-input-between-1-and-100). - Adds a `superRefine`d `DatabaseFormSchema` in `PerformanceSettingsForm.tsx` that blocks submission when `DB_MAX_POOL_SIZE_UNIT === 'percent'` and the value falls outside 1-100. Connections (fixed pool size) path keeps the existing `min(1)` behavior per the ticket. - Updates the number input's `min`/`max` attributes to match: percent is now `1`-`100` (was `3`-`80`); connections unchanged (`3`-`Math.floor(maxConnectionLimit * 0.8)`). ## Test plan - [x] Unit tests added in `PerformanceSettingsForm.test.ts` covering bounds (1, 100), out-of-range (0, 101, -5, 150), mid-range, string coercion, connections path (no upper zod bound), and enum rejection. All 12 pass. - [x] Manually verify in Auth → Performance settings that entering 0 or 101 in percent mode shows a validation error and blocks save, and that switching to absolute connections still uses the existing bounds. ## Summary by CodeRabbit * **Bug Fixes** * Refined database pool-size validation and input constraints; percentage unit enforces values between 1–100 and rejects zero, negative, and out-of-range entries. String inputs that represent numbers are coerced where appropriate; unknown unit values are rejected. * **Tests** * Added comprehensive tests for boundary, invalid, string, and unknown-unit cases and for validation error messages. * **Refactor** * Validation moved to a centralized schema and HTML min/max input attributes were removed. --- .../Auth/PerformanceSettingsForm.test.ts | 111 ++++++++++++++++++ .../Auth/PerformanceSettingsForm.tsx | 30 +++-- 2 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 apps/studio/components/interfaces/Auth/PerformanceSettingsForm.test.ts diff --git a/apps/studio/components/interfaces/Auth/PerformanceSettingsForm.test.ts b/apps/studio/components/interfaces/Auth/PerformanceSettingsForm.test.ts new file mode 100644 index 00000000000..2cd87a61879 --- /dev/null +++ b/apps/studio/components/interfaces/Auth/PerformanceSettingsForm.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, it } from 'vitest' + +import { DatabaseFormSchema } from './PerformanceSettingsForm' + +describe('DatabaseFormSchema', () => { + describe('percent unit', () => { + it('accepts 1 (lower bound)', () => { + const result = DatabaseFormSchema.safeParse({ + DB_MAX_POOL_SIZE: 1, + DB_MAX_POOL_SIZE_UNIT: 'percent', + }) + expect(result.success).toBe(true) + }) + + it('accepts 100 (upper bound)', () => { + const result = DatabaseFormSchema.safeParse({ + DB_MAX_POOL_SIZE: 100, + DB_MAX_POOL_SIZE_UNIT: 'percent', + }) + expect(result.success).toBe(true) + }) + + it('accepts a mid-range value', () => { + const result = DatabaseFormSchema.safeParse({ + DB_MAX_POOL_SIZE: 50, + DB_MAX_POOL_SIZE_UNIT: 'percent', + }) + expect(result.success).toBe(true) + }) + + it('rejects 0', () => { + const result = DatabaseFormSchema.safeParse({ + DB_MAX_POOL_SIZE: 0, + DB_MAX_POOL_SIZE_UNIT: 'percent', + }) + expect(result.success).toBe(false) + }) + + it('rejects 101', () => { + const result = DatabaseFormSchema.safeParse({ + DB_MAX_POOL_SIZE: 101, + DB_MAX_POOL_SIZE_UNIT: 'percent', + }) + expect(result.success).toBe(false) + if (!result.success) { + const issue = result.error.issues.find((i) => i.path[0] === 'DB_MAX_POOL_SIZE') + expect(issue?.message).toBe('Percentage must be between 1 and 100') + } + }) + + it('rejects negative values', () => { + const result = DatabaseFormSchema.safeParse({ + DB_MAX_POOL_SIZE: -5, + DB_MAX_POOL_SIZE_UNIT: 'percent', + }) + expect(result.success).toBe(false) + }) + + it('coerces string input', () => { + const result = DatabaseFormSchema.safeParse({ + DB_MAX_POOL_SIZE: '75', + DB_MAX_POOL_SIZE_UNIT: 'percent', + }) + expect(result.success).toBe(true) + }) + + it('rejects out-of-range string input', () => { + const result = DatabaseFormSchema.safeParse({ + DB_MAX_POOL_SIZE: '150', + DB_MAX_POOL_SIZE_UNIT: 'percent', + }) + expect(result.success).toBe(false) + }) + }) + + describe('connections unit', () => { + it('accepts 1 (min)', () => { + const result = DatabaseFormSchema.safeParse({ + DB_MAX_POOL_SIZE: 1, + DB_MAX_POOL_SIZE_UNIT: 'connections', + }) + expect(result.success).toBe(true) + }) + + it('accepts values above 100 (no upper schema bound for absolute)', () => { + const result = DatabaseFormSchema.safeParse({ + DB_MAX_POOL_SIZE: 500, + DB_MAX_POOL_SIZE_UNIT: 'connections', + }) + expect(result.success).toBe(true) + }) + + it('rejects 0', () => { + const result = DatabaseFormSchema.safeParse({ + DB_MAX_POOL_SIZE: 0, + DB_MAX_POOL_SIZE_UNIT: 'connections', + }) + expect(result.success).toBe(false) + }) + }) + + describe('unit enum', () => { + it('rejects unknown unit values', () => { + const result = DatabaseFormSchema.safeParse({ + DB_MAX_POOL_SIZE: 10, + DB_MAX_POOL_SIZE_UNIT: 'bytes', + }) + expect(result.success).toBe(false) + }) + }) +}) diff --git a/apps/studio/components/interfaces/Auth/PerformanceSettingsForm.tsx b/apps/studio/components/interfaces/Auth/PerformanceSettingsForm.tsx index 71fe409e6e3..9235a5f47e8 100644 --- a/apps/studio/components/interfaces/Auth/PerformanceSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/PerformanceSettingsForm.tsx @@ -46,6 +46,23 @@ const FormSchema = z.object({ DB_MAX_POOL_SIZE_UNIT: z.enum(['percent', 'connections']), }) +export const DatabaseFormSchema = z + .object({ + DB_MAX_POOL_SIZE: z.coerce.number().min(1), + DB_MAX_POOL_SIZE_UNIT: z.enum(['percent', 'connections']), + }) + .superRefine((data, ctx) => { + if (data.DB_MAX_POOL_SIZE_UNIT === 'percent') { + if (data.DB_MAX_POOL_SIZE < 1 || data.DB_MAX_POOL_SIZE > 100) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['DB_MAX_POOL_SIZE'], + message: 'Percentage must be between 1 and 100', + }) + } + } + }) + export const PerformanceSettingsForm = () => { const { data: project } = useSelectedProjectQuery() const { hasAccess: hasAccessToPerformance, isLoading: isLoadingEntitlement } = @@ -87,12 +104,7 @@ export const PerformanceSettingsForm = () => { }) const databaseForm = useForm({ - resolver: zodResolver( - z.object({ - DB_MAX_POOL_SIZE: FormSchema.shape.DB_MAX_POOL_SIZE, - DB_MAX_POOL_SIZE_UNIT: FormSchema.shape.DB_MAX_POOL_SIZE_UNIT, - }) - ), + resolver: zodResolver(DatabaseFormSchema), defaultValues: { DB_MAX_POOL_SIZE: 10, DB_MAX_POOL_SIZE_UNIT: 'connections', @@ -375,12 +387,6 @@ export const PerformanceSettingsForm = () => {