Skip to content
Open
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
32 changes: 30 additions & 2 deletions backend/src/config/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ describe('Config Settings', () => {
'POWERSYNC_JWT_KID',
'POWERSYNC_JWT_SECRET',
'POWERSYNC_TOKEN_EXPIRY_SECONDS',
'NODE_ENV',
] as const

let savedEnv: Partial<Record<string, string>>
Expand All @@ -444,16 +445,29 @@ describe('Config Settings', () => {
clearSettingsCache()
})

it('should use default values when PowerSync env vars are unset', () => {
it('should default PowerSync env vars to empty strings outside development', () => {
for (const key of POWERSYNC_ENV_KEYS) {
delete process.env[key]
}
const settings = getSettings()

expect(settings.powersyncUrl).toBe('')
expect(settings.powersyncJwtKid).toBe('')
expect(settings.powersyncJwtSecret).toBe('')
expect(settings.powersyncTokenExpirySeconds).toBe(3600)
})

it('should use localhost defaults when NODE_ENV=development', () => {
for (const key of POWERSYNC_ENV_KEYS) {
delete process.env[key]
}
process.env.NODE_ENV = 'development'

const settings = getSettings()

expect(settings.powersyncUrl).toBe('http://localhost:8080')
expect(settings.powersyncJwtKid).toBe('powersync-dev')
expect(settings.powersyncJwtSecret).toBe('powersync-dev-secret-change-in-production')
expect(settings.powersyncTokenExpirySeconds).toBe(3600)
})

it('should read PowerSync values from env when set', () => {
Expand Down Expand Up @@ -505,6 +519,20 @@ describe('Config Settings', () => {
process.env.POWERSYNC_JWT_SECRET = 'a'.repeat(32)
expect(() => getSettings()).not.toThrow()
})

it('should allow empty JWT secret when powersyncUrl is empty', () => {
process.env.POWERSYNC_URL = ''
process.env.POWERSYNC_JWT_SECRET = ''
const settings = getSettings()
expect(settings.powersyncJwtSecret).toBe('')
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test doesn't control NODE_ENV, fails in development

Low Severity

The new test "should allow empty JWT secret when powersyncUrl is empty" sets POWERSYNC_URL and POWERSYNC_JWT_SECRET to '' but doesn't set or delete NODE_ENV. Since empty strings are falsy and fall through the || in parseSettings, when NODE_ENV happens to be 'development', the dev defaults ('http://localhost:8080' and 'powersync-dev-secret-change-in-production') are used instead. The assertion expect(settings.powersyncJwtSecret).toBe('') then fails. Other tests in this block explicitly delete or set NODE_ENV; this one is missing that step.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4b03fc4. Configure here.


it('should reject empty JWT secret in non-dev when POWERSYNC_URL is set', () => {
delete process.env.NODE_ENV
process.env.POWERSYNC_URL = 'https://sync.example.com'
delete process.env.POWERSYNC_JWT_SECRET
expect(() => getSettings()).toThrow()
})
})

describe('isOAuthRedirectUriAllowed', () => {
Expand Down
19 changes: 12 additions & 7 deletions backend/src/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ const settingsSchema = z
waitlistEnabled: z.boolean().default(false),
waitlistAutoApproveDomains: z.string().default(''),

// PowerSync settings — defaults match the local dev stack in powersync-service/config/config.yaml
powersyncUrl: z.string().default('http://localhost:8080'),
powersyncJwtKid: z.string().default('powersync-dev'),
powersyncJwtSecret: z.string().default('powersync-dev-secret-change-in-production'),
// PowerSync settings
powersyncUrl: z.string().default(''),
powersyncJwtKid: z.string().default(''),
powersyncJwtSecret: z.string().default(''),
powersyncTokenExpirySeconds: z.coerce.number().int().positive().default(3600),

// CORS settings — comma-separated list of exact origins.
Expand Down Expand Up @@ -113,6 +113,7 @@ export type Settings = z.infer<typeof settingsSchema>
* Parse and validate environment variables into settings
*/
const parseSettings = (): Settings => {
const isDevelopment = process.env.NODE_ENV === 'development'
const env = {
fireworksApiKey: process.env.FIREWORKS_API_KEY || '',
mistralApiKey: process.env.MISTRAL_API_KEY || '',
Expand Down Expand Up @@ -143,9 +144,13 @@ const parseSettings = (): Settings => {
posthogApiKey: process.env.POSTHOG_API_KEY || '',
waitlistEnabled: process.env.WAITLIST_ENABLED === 'true',
waitlistAutoApproveDomains: process.env.WAITLIST_AUTO_APPROVE_DOMAINS || '',
powersyncUrl: process.env.POWERSYNC_URL || 'http://localhost:8080',
powersyncJwtKid: process.env.POWERSYNC_JWT_KID || 'powersync-dev',
powersyncJwtSecret: process.env.POWERSYNC_JWT_SECRET || 'powersync-dev-secret-change-in-production',
// Localhost defaults apply only in development. In any other NODE_ENV the
// value defaults to '' so the schema's superRefine guard correctly rejects
// an empty JWT secret whenever POWERSYNC_URL is set explicitly.
powersyncUrl: process.env.POWERSYNC_URL || (isDevelopment ? 'http://localhost:8080' : ''),
powersyncJwtKid: process.env.POWERSYNC_JWT_KID || (isDevelopment ? 'powersync-dev' : ''),
powersyncJwtSecret:
process.env.POWERSYNC_JWT_SECRET || (isDevelopment ? 'powersync-dev-secret-change-in-production' : ''),
powersyncTokenExpirySeconds: process.env.POWERSYNC_TOKEN_EXPIRY_SECONDS || '3600',
corsOrigins: process.env.CORS_ORIGINS || 'http://localhost:1420,tauri://localhost,http://tauri.localhost',
corsAllowCredentials: process.env.CORS_ALLOW_CREDENTIALS !== 'false',
Expand Down