Files
supabase/apps/studio/lib/password-strength.test.ts
T
Alaister YoungandAlaister Young 46b31eb53a [FE-3379] feat(studio): warn when db passwords need percent-encoding (#47564)
Users who set a database password with special characters (\`@\`, \`#\`,
\`%\`, \`+\`, etc.) get no warning that it must be percent-encoded when
used in a connection URL, which leads to confusing connection failures
([FE-3379](https://linear.app/supabase/issue/FE-3379)).

<img width="700" height="200" alt="Screenshot 2026-07-03 at 6 26 43 PM"
src="https://github.com/user-attachments/assets/48608d65-8057-4abe-96fc-c0ede3550951"
/>
<img width="1002" height="395" alt="Screenshot 2026-07-03 at 6 27 14 PM"
src="https://github.com/user-attachments/assets/1366b985-7d80-4e7d-97f0-c79d5c84cefd"
/>
<img width="548" height="303" alt="Screenshot 2026-07-03 at 6 27 26 PM"
src="https://github.com/user-attachments/assets/b042101a-0e88-4730-adb8-1b490018f208"
/>

**Changed:**
- `PasswordStrengthBar` now shows a warning-colored callout (with a docs
link) whenever the entered password contains characters that need
percent-encoding — this covers project creation, reset database
password, restore-to-new-project, and the Vercel deploy-button flow
- Replaced `DATABASE_PASSWORD_REGEX` (only caught `@`, `:`, `/`) with a
`passwordNeedsPercentEncoding()` helper based on `encodeURIComponent`,
so `#`, `%`, `+`, `?`, `&`, spaces etc. are caught too
- Moved `SpecialSymbolsCallout` from `ProjectCreation/` to
`components/ui/` since it's now shared

**Added:**
- Info admonition in the Connect sheet next to connection strings that
still contain `[YOUR-PASSWORD]` (direct connection + `.env`-based file
setups; hidden for psql and .NET where percent-encoding doesn't apply,
and after a password reset since the substituted password is already
encoded)

## To test

- Project creation → type a password containing \`#\` or \`@\` → warning
callout appears above the strength bar; disappears for alphanumeric
passwords
- Database Settings → Reset database password → same behaviour
- Connect sheet → Direct connection → note shows under the connection
string for URI/JDBC types, not for psql; after resetting the password
from the sheet, the note disappears (password is substituted already
encoded)
- Connect sheet → Node.js/Python/Go/SQLAlchemy file setups show the
note; .NET does not
- \`pnpm vitest run lib/password-strength.test.ts\` passes

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **New Features**
* Added a dedicated password encoding note (with documentation link) on
direct connection screens when the password is embedded in a URL.
* Added an encoding hint to the password strength area when
percent-encoding is required.
* **Bug Fixes**
* Removed regex-based “invalid password” callout and replaced it with
safer percent-encoding detection logic.
* **Tests**
  * Added test coverage for `passwordNeedsPercentEncoding`.
  * Removed obsolete Project Creation password regex tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-07-06 17:38:03 +08:00

56 lines
2.5 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { passwordNeedsPercentEncoding, passwordStrength } from './password-strength'
describe('passwordNeedsPercentEncoding', () => {
it('returns false for passwords that are safe to use in a connection string', () => {
expect(passwordNeedsPercentEncoding('')).toBe(false)
expect(passwordNeedsPercentEncoding('teststring')).toBe(false)
expect(passwordNeedsPercentEncoding('Str0ngPassword123')).toBe(false)
expect(passwordNeedsPercentEncoding('with-safe_chars.~!')).toBe(false)
})
it('returns true for passwords with characters that need percent-encoding', () => {
expect(passwordNeedsPercentEncoding('test@string')).toBe(true)
expect(passwordNeedsPercentEncoding('te:ststring')).toBe(true)
expect(passwordNeedsPercentEncoding('tests/tring')).toBe(true)
expect(passwordNeedsPercentEncoding('test#string')).toBe(true)
expect(passwordNeedsPercentEncoding('test%string')).toBe(true)
expect(passwordNeedsPercentEncoding('test+string')).toBe(true)
expect(passwordNeedsPercentEncoding('test?string')).toBe(true)
expect(passwordNeedsPercentEncoding('test&string')).toBe(true)
expect(passwordNeedsPercentEncoding('test string')).toBe(true)
})
})
describe('passwordStrength', () => {
it('returns empty values for message, warning and strength for empty input', async () => {
const result = await passwordStrength('')
expect(result).toEqual({ message: '', warning: '', strength: 0 })
})
it('returns max length message, warning, and strength 0 for password longer than 99 characters', async () => {
const longPassword = 'a'.repeat(100)
const result = await passwordStrength(longPassword)
expect(result.message).toMatch(/maximum length/i)
expect(result.warning).toMatch(/less than 100 characters/i)
expect(result.strength).toBe(0)
})
it('returns strong score, suggestion, and empty warning for strong password', async () => {
const result = await passwordStrength('ActuallyAStrongPassword123!')
expect(result.message).toMatch(/strong/i)
expect(result.message).toContain('This password is strong')
expect(result.warning).toBe('')
expect(result.strength).toBe(4)
})
it('returns weak score, suggestion, and warning for weak password', async () => {
const result = await passwordStrength('weak')
expect(result.message).toMatch(/not secure/i)
expect(result.message).toContain('This password is not secure enough')
expect(result.warning).toMatch(/you need a stronger password/i)
expect(result.strength).toBe(1)
})
})