Add security-focused tests for Login Page error messages (#1005)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
This commit is contained in:
211
app/src/components/LoginDialog.security.spec.tsx
Normal file
211
app/src/components/LoginDialog.security.spec.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* LoginDialog Security Tests
|
||||
*
|
||||
* Security-focused tests for the Login Page:
|
||||
* - Error message visibility to users
|
||||
* - Rate limiting enforcement (anti-brute force)
|
||||
* - Credential requirement validation
|
||||
* - Information disclosure prevention
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { expect } from 'chai'
|
||||
import { describe, it } from 'mocha'
|
||||
import { LoginDialog } from './LoginDialog'
|
||||
import { renderWithProviders, waitFor } from '../utils/spec/testUtils'
|
||||
|
||||
// Helper to get elements
|
||||
const getByText = (text: string) => {
|
||||
const elements = Array.from(document.querySelectorAll('*'))
|
||||
return elements.find(el => el.textContent?.includes(text))
|
||||
}
|
||||
const getByTestId = (testId: string) => document.querySelector(`[data-testid="${testId}"]`)
|
||||
|
||||
describe('LoginDialog Security Tests', () => {
|
||||
describe('Error Message Visibility (Security)', () => {
|
||||
it('should display "Invalid credentials" error message to user', () => {
|
||||
const mockLogin = () => {}
|
||||
const errorMessage = 'Invalid credentials'
|
||||
|
||||
renderWithProviders(
|
||||
<LoginDialog open={true} onLogin={mockLogin} error={errorMessage} />,
|
||||
{ withTheme: true }
|
||||
)
|
||||
|
||||
// Verify error is visible to user
|
||||
const errorElement = getByText(errorMessage)
|
||||
expect(errorElement).to.exist
|
||||
})
|
||||
|
||||
it('should display rate limiting error message to user', () => {
|
||||
const mockLogin = () => {}
|
||||
const errorMessage = 'Too many failed authentication attempts. Please wait 30 seconds before trying again.'
|
||||
|
||||
renderWithProviders(
|
||||
<LoginDialog open={true} onLogin={mockLogin} error={errorMessage} />,
|
||||
{ withTheme: true }
|
||||
)
|
||||
|
||||
// Verify rate limiting error is visible to user
|
||||
expect(getByText('Too many failed authentication attempts')).to.exist
|
||||
})
|
||||
|
||||
it('should display "Authentication required" error message to user', () => {
|
||||
const mockLogin = () => {}
|
||||
const errorMessage = 'Please enter your username and password.'
|
||||
|
||||
renderWithProviders(
|
||||
<LoginDialog open={true} onLogin={mockLogin} error={errorMessage} />,
|
||||
{ withTheme: true }
|
||||
)
|
||||
|
||||
// Verify auth required message is visible to user
|
||||
expect(getByText('Please enter your username and password.')).to.exist
|
||||
})
|
||||
|
||||
it('should display generic authentication failure message to user', () => {
|
||||
const mockLogin = () => {}
|
||||
const errorMessage = 'Authentication failed. Please try again.'
|
||||
|
||||
renderWithProviders(
|
||||
<LoginDialog open={true} onLogin={mockLogin} error={errorMessage} />,
|
||||
{ withTheme: true }
|
||||
)
|
||||
|
||||
// Verify generic error is visible to user
|
||||
expect(getByText('Authentication failed. Please try again.')).to.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rate Limiting Enforcement (Anti-Brute Force)', () => {
|
||||
it('should disable login button during rate limit countdown', () => {
|
||||
const mockLogin = () => {}
|
||||
const waitTime = 30
|
||||
|
||||
renderWithProviders(
|
||||
<LoginDialog open={true} onLogin={mockLogin} waitTimeSeconds={waitTime} />,
|
||||
{ withTheme: true }
|
||||
)
|
||||
|
||||
// Verify button is disabled to prevent further attempts
|
||||
const buttons = Array.from(document.querySelectorAll('button'))
|
||||
const loginButton = buttons.find(b => b.textContent?.match(/Wait \d+s/))
|
||||
expect(loginButton).to.exist
|
||||
expect(loginButton?.hasAttribute('disabled')).to.be.true
|
||||
})
|
||||
|
||||
it('should disable input fields during rate limit countdown', () => {
|
||||
const mockLogin = () => {}
|
||||
const waitTime = 30
|
||||
|
||||
renderWithProviders(
|
||||
<LoginDialog open={true} onLogin={mockLogin} waitTimeSeconds={waitTime} />,
|
||||
{ withTheme: true }
|
||||
)
|
||||
|
||||
// Verify inputs are disabled to prevent modification during lockout
|
||||
const usernameInput = getByTestId('username-input')?.querySelector('input')
|
||||
const passwordInput = getByTestId('password-input')?.querySelector('input')
|
||||
|
||||
expect(usernameInput?.hasAttribute('disabled')).to.be.true
|
||||
expect(passwordInput?.hasAttribute('disabled')).to.be.true
|
||||
})
|
||||
|
||||
it('should display countdown timer to user during rate limiting', () => {
|
||||
const mockLogin = () => {}
|
||||
const waitTime = 30
|
||||
|
||||
renderWithProviders(
|
||||
<LoginDialog open={true} onLogin={mockLogin} waitTimeSeconds={waitTime} />,
|
||||
{ withTheme: true }
|
||||
)
|
||||
|
||||
// Verify countdown is visible to inform user of lockout duration
|
||||
const countdownElement = getByText('Please wait')
|
||||
expect(countdownElement).to.exist
|
||||
expect(countdownElement?.textContent).to.match(/Please wait \d+ seconds before trying again/i)
|
||||
})
|
||||
|
||||
it('should display both rate limit error and countdown to user', () => {
|
||||
const mockLogin = () => {}
|
||||
const errorMessage = 'Too many failed authentication attempts. Please wait 30 seconds before trying again.'
|
||||
const waitTime = 30
|
||||
|
||||
renderWithProviders(
|
||||
<LoginDialog open={true} onLogin={mockLogin} error={errorMessage} waitTimeSeconds={waitTime} />,
|
||||
{ withTheme: true }
|
||||
)
|
||||
|
||||
// Verify both error and countdown are visible
|
||||
expect(getByText(errorMessage)).to.exist
|
||||
expect(getByText('Please wait 30 seconds before trying again')).to.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('Credential Requirement Validation (Prevent Unauthorized Access)', () => {
|
||||
it('should require both username and password fields to be present', () => {
|
||||
const mockLogin = () => {}
|
||||
|
||||
renderWithProviders(
|
||||
<LoginDialog open={true} onLogin={mockLogin} />,
|
||||
{ withTheme: true }
|
||||
)
|
||||
|
||||
// Verify both credential fields exist and are required
|
||||
const usernameInput = getByTestId('username-input')
|
||||
const passwordInput = getByTestId('password-input')
|
||||
|
||||
expect(usernameInput).to.exist
|
||||
expect(passwordInput).to.exist
|
||||
})
|
||||
|
||||
it('should require password field to be masked', () => {
|
||||
const mockLogin = () => {}
|
||||
|
||||
renderWithProviders(
|
||||
<LoginDialog open={true} onLogin={mockLogin} />,
|
||||
{ withTheme: true }
|
||||
)
|
||||
|
||||
// Verify password is masked (type="password") for security
|
||||
const passwordInput = getByTestId('password-input')?.querySelector('input')
|
||||
expect(passwordInput?.getAttribute('type')).to.equal('password')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Information Disclosure Prevention', () => {
|
||||
it('should use generic "Invalid credentials" error (no username enumeration)', () => {
|
||||
const mockLogin = () => {}
|
||||
// Error doesn't distinguish between invalid username vs invalid password
|
||||
const errorMessage = 'Invalid credentials'
|
||||
|
||||
renderWithProviders(
|
||||
<LoginDialog open={true} onLogin={mockLogin} error={errorMessage} />,
|
||||
{ withTheme: true }
|
||||
)
|
||||
|
||||
// Verify error doesn't leak whether username or password was wrong
|
||||
const errorElement = getByText(errorMessage)
|
||||
expect(errorElement).to.exist
|
||||
expect(errorElement?.textContent).to.not.include('username')
|
||||
expect(errorElement?.textContent).to.not.include('password')
|
||||
})
|
||||
|
||||
it('should not display sensitive information in error messages', () => {
|
||||
const mockLogin = () => {}
|
||||
const errorMessage = 'Invalid credentials'
|
||||
|
||||
renderWithProviders(
|
||||
<LoginDialog open={true} onLogin={mockLogin} error={errorMessage} />,
|
||||
{ withTheme: true }
|
||||
)
|
||||
|
||||
// Verify error doesn't contain sensitive data
|
||||
const errorElement = getByText(errorMessage)
|
||||
expect(errorElement?.textContent).to.not.include('database')
|
||||
expect(errorElement?.textContent).to.not.include('server')
|
||||
expect(errorElement?.textContent).to.not.include('SQL')
|
||||
expect(errorElement?.textContent).to.not.include('error code')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user