Definition
- Happy path – Valid credentials lead to the dashboard. This is one single case out of dozens.
- Negative credential matrix – Invalid username, invalid password, both invalid, empty fields, whitespace-only input, each returning the same generic error message to prevent account enumeration.
- Security – Account lockout after N failed attempts, SQL-injection and XSS inputs safely rejected, password masking, no information leaks.
- Session – Remember-me with secure cookies, session timeout after idle, logout that invalidates the server session.
- Recovery – Password-reset link that is single-use, time-limited, and does not reveal whether the email exists.
- Non-functional – Browser compatibility across the supported matrix, accessibility with keyboard navigation, ARIA roles, and screen-reader announcements.
For the National Bank of India login screen, the happy path is Rajesh typing his correct username and password and getting in. But a real tester asks all the uncomfortable questions: what if the password is wrong, what if both fields are empty, what if someone tries the wrong password ten times to break in, does the password show as dots so a shoulder-surfer cannot read it, does "remember me" actually keep him logged in, does the session expire after inactivity so an unattended machine is safe, can an attacker type a database trick or a script into the username box, and does the error message say just "invalid credentials" rather than revealing whether the username exists. Covering all of these is what separates a real tester from someone who only checks that the front door opens for the right key.
Understanding the Concepts
What is the happy path for login?
The happy path is a single case: valid username and valid password lead to a successful login and a redirect to the dashboard. Everything interesting is around it.
// tests/login/happy-path.spec.ts
import { test, expect } from '@playwright/test';
test('Valid credentials redirect to dashboard', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Username').fill(process.env.TEST_USER!);
await page.getByLabel('Password').fill(process.env.TEST_PASS!);
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
await expect(page.getByText('Account Balance')).toBeVisible();
});
What negative credential cases should you test?
The negative credential matrix covers valid username with invalid password, invalid username with any password, both invalid, and the critical rule that all failures return the same generic message so an attacker cannot tell which part was wrong.
// tests/login/negative-credentials.spec.ts
const negativeCases = [
{ user: 'rajesh@nationalbank.in', pass: 'wrong', desc: 'valid user, wrong pass' },
{ user: 'nobody@nationalbank.in', pass: 'any', desc: 'unknown user' },
{ user: 'nobody@nationalbank.in', pass: 'wrong', desc: 'both invalid' },
{ user: '', pass: '', desc: 'both empty' },
{ user: ' ', pass: ' ', desc: 'whitespace only' },
];
for (const { user, pass, desc } of negativeCases) {
test(`Negative: ${desc}`, async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Username').fill(user);
await page.getByLabel('Password').fill(pass);
await page.getByRole('button', { name: 'Sign In' }).click();
// Same generic message for all failures (anti-enumeration)
await expect(page.getByText('Invalid credentials')).toBeVisible();
// Must NOT reveal which field was wrong
await expect(page.getByText('Username not found')).not.toBeVisible();
await expect(page).toHaveURL(/\/login/);
});
}
What security cases should you test?
Security behaviors are central for a bank. Account lockout, injection resistance, and password masking protect customer accounts.
// tests/login/security.spec.ts
test('Account locks after 5 failed attempts', async ({ page }) => {
await page.goto('/login');
for (let i = 0; i < 5; i++) {
await page.getByLabel('Username').fill('rajesh@nationalbank.in');
await page.getByLabel('Password').fill('wrong-' + i);
await page.getByRole('button', { name: 'Sign In' }).click();
}
// 6th attempt with CORRECT password must still be refused
await page.getByLabel('Username').fill('rajesh@nationalbank.in');
await page.getByLabel('Password').fill(process.env.TEST_PASS!);
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page.getByText('Account locked')).toBeVisible();
});
test('SQL injection in username is rejected safely', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Username').fill("' OR 1=1 --");
await page.getByLabel('Password').fill('anything');
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page.getByText('Invalid credentials')).toBeVisible();
// Must NOT show database error or stack trace
await expect(page.locator('body')).not.toContainText('SQL');
await expect(page.locator('body')).not.toContainText('Exception');
});
test('XSS in username is escaped', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Username').fill('<script>alert(1)</script>');
await page.getByLabel('Password').fill('anything');
await page.getByRole('button', { name: 'Sign In' }).click();
// No script execution, no alert dialog
await expect(page.getByText('Invalid credentials')).toBeVisible();
});
test('Password field is masked', async ({ page }) => {
await page.goto('/login');
const passwordInput = page.getByLabel('Password');
await expect(passwordInput).toHaveAttribute('type', 'password');
});
What session management cases should you test?
Session behaviors include remember-me, session timeout, and logout. Each protects the user's authenticated state.
// tests/login/session.spec.ts
test('Remember-me persists session across browser restart', async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('/login');
await page.getByLabel('Username').fill(process.env.TEST_USER!);
await page.getByLabel('Password').fill(process.env.TEST_PASS!);
await page.getByLabel('Remember me').check();
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page).toHaveURL(/\/dashboard/);
// Save cookies, close context, open new one with same cookies
const cookies = await context.cookies();
await context.close();
const newContext = await browser.newContext();
await newContext.addCookies(cookies);
const newPage = await newContext.newPage();
await newPage.goto('/dashboard');
await expect(newPage).toHaveURL(/\/dashboard/);
await newContext.close();
});
test('Logout invalidates server session', async ({ page }) => {
// Login first
await page.goto('/login');
await page.getByLabel('Username').fill(process.env.TEST_USER!);
await page.getByLabel('Password').fill(process.env.TEST_PASS!);
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page).toHaveURL(/\/dashboard/);
// Logout
await page.getByRole('button', { name: 'Logout' }).click();
await expect(page).toHaveURL(/\/login/);
// Back button must NOT reach dashboard
await page.goBack();
await expect(page).toHaveURL(/\/login/);
});
What password reset cases should you test?
The password-reset flow must be secure: single-use links, time-limited, and no account enumeration.
// tests/login/password-reset.spec.ts
test('Password reset link is single-use', async ({ page, request }) => {
// Request a reset
await page.goto('/forgot-password');
await page.getByLabel('Email').fill('rajesh@nationalbank.in');
await page.getByRole('button', { name: 'Send Reset Link' }).click();
await expect(page.getByText('Check your email')).toBeVisible();
// Simulate using the reset link (via API to get the token)
const token = await getResetToken('rajesh@nationalbank.in');
await page.goto(`/reset-password?token=${token}`);
await page.getByLabel('New Password').fill('NewPass123!');
await page.getByRole('button', { name: 'Reset' }).click();
await expect(page.getByText('Password updated')).toBeVisible();
// Same link again must be rejected
await page.goto(`/reset-password?token=${token}`);
await expect(page.getByText('Link expired')).toBeVisible();
});
test('Reset for non-existent email does not reveal account existence', async ({ page }) => {
await page.goto('/forgot-password');
await page.getByLabel('Email').fill('nobody@nationalbank.in');
await page.getByRole('button', { name: 'Send Reset Link' }).click();
// Same message whether the account exists or not
await expect(page.getByText('Check your email')).toBeVisible();
});
What accessibility and browser compatibility cases should you test?
Non-functional checks confirm the login page works for all users across all supported browsers.
// tests/login/accessibility.spec.ts
import AxeBuilder from '@axe-core/playwright';
test('Login page passes accessibility audit', async ({ page }) => {
await page.goto('/login');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
test('Login form is keyboard navigable', async ({ page }) => {
await page.goto('/login');
await page.keyboard.press('Tab');
await expect(page.getByLabel('Username')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByLabel('Password')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByRole('button', { name: 'Sign In' })).toBeFocused();
await page.keyboard.press('Enter');
});
# playwright.config.ts projects for browser compatibility
projects:
- name: chromium
use: { browserName: 'chromium' }
- name: firefox
use: { browserName: 'firefox' }
- name: webkit
use: { browserName: 'webkit' }
What error message rules should you enforce?
Precise, non-leaky error messages protect customers and comply with security standards.
- Never say "username not found" or "wrong password" separately. Always say "invalid credentials" so attackers cannot enumerate accounts.
- Never expose stack traces, SQL errors, or internal details.
- Use a stable error code alongside the human message so API consumers can programmatically handle errors.
// Correct: generic error response
package com.bank.auth;
public class LoginResponse {
// WRONG: reveals which field failed
// public String error = "Username not found";
// CORRECT: generic, non-leaky
public String error = "Invalid credentials";
public String errorCode = "AUTH_001";
}
What is the interviewer actually evaluating?
The interviewer is assessing breadth of thinking and risk awareness, not whether you know login works. They want to see that you systematically cover:
| Category | Cases | Risk Level |
|---|---|---|
| Positive | Valid credentials, dashboard redirect | Baseline |
| Negative | Wrong password, unknown user, empty fields | Medium |
| Security | Lockout, injection, XSS, masking, enumeration | Critical |
| Session | Remember-me, timeout, logout | High |
| Recovery | Password reset, single-use link | High |
| Non-functional | Browsers, accessibility, error messages | Medium |
A strong answer groups cases into these families and explains the risk behind each, especially the security and session ones, because for a bank the refusal path is as important as the success path.
What are common interview follow-up questions?
- "How many test cases would you write for login?" Between 20 and 40, grouped into the six families above.
- "Which cases would you automate?" The negative matrix, security, and session cases, because they are repetitive and critical. Keep exploratory accessibility testing manual.
- "What is the most important login test case?" Account lockout and anti-enumeration, because these directly prevent unauthorized access to customer accounts.
Visual Overview
Login Test Case Families
────────────────────────
┌─────────────────────────────────────────┐
│ POSITIVE (1 case) │
│ Valid user + valid pass -> dashboard │
└─────────────────────────────────────────┘
│
┌────────┴──────────────────────────────────┐
│ NEGATIVE (5+ cases) │
│ Wrong pass, unknown user, both invalid, │
│ empty fields, whitespace │
│ All return: "Invalid credentials" │
└───────────────────────────────────────────┘
│
┌────────┴──────────────────────────────────┐
│ SECURITY (5+ cases) │
│ Lockout after N failures │
│ SQL injection rejected │
│ XSS escaped │
│ Password masked │
│ No account enumeration │
└───────────────────────────────────────────┘
│
┌────────┴──────────────────────────────────┐
│ SESSION (3+ cases) │
│ Remember-me persists │
│ Session timeout forces re-auth │
│ Logout invalidates server session │
└───────────────────────────────────────────┘
│
┌────────┴──────────────────────────────────┐
│ RECOVERY (2+ cases) │
│ Reset link is single-use + time-limited │
│ Non-existent email: same message │
└───────────────────────────────────────────┘
│
┌────────┴──────────────────────────────────┐
│ NON-FUNCTIONAL (3+ cases) │
│ Browser compatibility matrix │
│ Keyboard navigation + ARIA │
│ Accessibility audit (axe-core) │
└───────────────────────────────────────────┘
Key Takeaway
When I write login test cases I start from the position that the happy path, valid username and valid password leading to the dashboard, is a single case, and that everything valuable lies around it. I group the rest into families: negative credentials covering wrong password, unknown user, both invalid, and empty fields, all returning the same generic message so attackers cannot enumerate accounts; security covering account lockout at the boundary, SQL-injection and XSS inputs safely rejected, and password masking; session covering remember-me, timeout, and logout that fully invalidates the server session; recovery covering single-use, time-limited password-reset links; and non-functional covering browser compatibility and accessibility.
What the interviewer is really evaluating is whether I think past "login works." Anyone can confirm the front door opens for the right key; a strong tester enumerates how the feature fails and how it is abused, prioritizes by risk, and writes each case with precise preconditions, data, steps, and an exact expected result.
At the National Bank of India I owned the login test suite, and I caught two real issues: a response that revealed whether a username existed, which I flagged as an account-enumeration vulnerability, and a logout that did not invalidate the server session so the back button still showed account data. Both were missed by happy-path testing and both mattered because this was the front door to customers' money.
Being able to lay out these case families and explain the risk behind each, especially the security and session ones, is exactly how I show an interviewer I test like someone who protects a bank, not someone who checks a checkbox.
Remember one sentence: the happy path is one case, the other thirty cases covering negatives, security, sessions, and recovery are where the real testing lives.
---