diff --git a/app/src/components/LoginDialog.security.spec.tsx b/app/src/components/LoginDialog.security.spec.tsx
new file mode 100644
index 0000000..d5134a3
--- /dev/null
+++ b/app/src/components/LoginDialog.security.spec.tsx
@@ -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(
+ ,
+ { 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(
+ ,
+ { 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(
+ ,
+ { 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(
+ ,
+ { 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(
+ ,
+ { 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(
+ ,
+ { 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(
+ ,
+ { 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(
+ ,
+ { 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(
+ ,
+ { 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(
+ ,
+ { 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(
+ ,
+ { 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(
+ ,
+ { 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')
+ })
+ })
+})