# Non-Functional Requirements (NFR) Criteria ## Principle Non-functional requirements (security, performance, reliability, maintainability) are **validated through automated tests**, not checklists. NFR assessment uses objective pass/fail criteria tied to measurable thresholds. Ambiguous requirements default to CONCERNS until clarified. ## Rationale **The Problem**: Teams ship features that "work" functionally but fail under load, expose security vulnerabilities, or lack error recovery. NFRs are treated as optional "nice-to-haves" instead of release blockers. **The Solution**: Define explicit NFR criteria with automated validation. Security tests verify auth/authz and secret handling. Performance tests enforce SLO/SLA thresholds with profiling evidence. Reliability tests validate error handling, retries, and health checks. Maintainability is measured by test coverage, code duplication, and observability. **Why This Matters**: - Prevents production incidents (security breaches, performance degradation, cascading failures) - Provides objective release criteria (no subjective "feels fast enough") - Automates compliance validation (audit trail for regulated environments) - Forces clarity on ambiguous requirements (default to CONCERNS) ## Pattern Examples ### Example 1: Security NFR Validation (Auth, Secrets, OWASP) **Context**: Automated security tests enforcing authentication, authorization, and secret handling **Implementation**: ```typescript // tests/nfr/security.spec.ts import { test, expect } from '@playwright/test'; test.describe('Security NFR: Authentication & Authorization', () => { test('unauthenticated users cannot access protected routes', async ({ page }) => { // Attempt to access dashboard without auth await page.goto('/dashboard'); // Should redirect to login (not expose data) await expect(page).toHaveURL(/\/login/); await expect(page.getByText('Please sign in')).toBeVisible(); // Verify no sensitive data leaked in response const pageContent = await page.content(); expect(pageContent).not.toContain('user_id'); expect(pageContent).not.toContain('api_key'); }); test('JWT tokens expire after 15 minutes', async ({ page, request }) => { // Login and capture token await page.goto('/login'); await page.getByLabel('Email').fill('test@example.com'); await page.getByLabel('Password').fill('ValidPass123!'); await page.getByRole('button', { name: 'Sign In' }).click(); const token = await page.evaluate(() => localStorage.getItem('auth_token')); expect(token).toBeTruthy(); // Wait 16 minutes (use mock clock in real tests) await page.clock.fastForward('00:16:00'); // Token should be expired, API call should fail const response = await request.get('/api/user/profile', { headers: { Authorization: `Bearer ${token}` }, }); expect(response.status()).toBe(401); const body = await response.json(); expect(body.error).toContain('expired'); }); test('passwords are never logged or exposed in errors', async ({ page }) => { // Trigger login error await page.goto('/login'); await page.getByLabel('Email').fill('test@example.com'); await page.getByLabel('Password').fill('WrongPassword123!'); // Monitor console for password leaks const consoleLogs: string[] = []; page.on('console', (msg) => consoleLogs.push(msg.text())); await page.getByRole('button', { name: 'Sign In' }).click(); // Error shown to user (generic message) await expect(page.getByText('Invalid credentials')).toBeVisible(); // Verify password NEVER appears in console, DOM, or network const pageContent = await page.content(); expect(pageContent).not.toContain('WrongPassword123!'); expect(consoleLogs.join('\n')).not.toContain('WrongPassword123!'); }); test('RBAC: users can only access resources they own', async ({ page, request }) => { // Login as User A const userAToken = await login(request, 'userA@example.com', 'password'); // Try to access User B's order const response = await request.get('/api/orders/user-b-order-id', { headers: { Authorization: `Bearer ${userAToken}` }, }); expect(response.status()).toBe(403); // Forbidden const body = await response.json(); expect(body.error).toContain('insufficient permissions'); }); test('SQL injection attempts are blocked', async ({ page }) => { await page.goto('/search'); // Attempt SQL injection await page.getByPlaceholder('Search products').fill("'; DROP TABLE users; --"); await page.getByRole('button', { name: 'Search' }).click(); // Should return empty results, NOT crash or expose error await expect(page.getByText('No results found')).toBeVisible(); // Verify app still works (table not dropped) await page.goto('/dashboard'); await expect(page.getByText('Welcome')).toBeVisible(); }); test('XSS attempts are sanitized', async ({ page }) => { await page.goto('/profile/edit'); // Attempt XSS injection const xssPayload = ''; await page.getByLabel('Bio').fill(xssPayload); await page.getByRole('button', { name: 'Save' }).click(); // Reload and verify XSS is escaped (not executed) await page.reload(); const bio = await page.getByTestId('user-bio').textContent(); // Text should be escaped, script should NOT execute expect(bio).toContain('<script>'); expect(bio).not.toContain('