Auth E2E Testing Skill
Executed by: Jayne (Tester)
Scope: Complete authentication flow on iOS (dev tunnel, local Aspire, simulator)
Duration: ~2.5 hours (all test cases)
Prerequisites: Aspire running locally, iOS simulator booted, dev tunnel active, webapp running
Test Infrastructure Setup
1.1 Verify Test Environment
Goal: Ensure all test prerequisites are met.
Steps:
- Confirm Aspire dashboard is accessible at
http://localhost:8080(or configured port) - Verify iOS simulator is booted and responsive:
xcrun simctl list devices | grep -i "iphone" - Confirm dev tunnel is live (visible in Aspire dashboard or terminal)
- Verify API URL in mobile app config points to dev tunnel:
https://c60qm31n-7012.use.devtunnels.ms - Confirm webapp is running (Blazor Server)
Expected Outcome:
- Aspire dashboard shows all services (Api, WebApp, Workers, Redis, SqliteDb) in running state
- Simulator shows iOS home screen (no crashes)
- API ping succeeds:
curl -s https://c60qm31n-7012.use.devtunnels.ms/health | jq .
Verification:
# Check API health
curl -s https://c60qm31n-7012.use.devtunnels.ms/health
# Check Aspire dashboard
curl -s http://localhost:8080 -w "\nStatus: %{http_code}\n"
Test Suite 1: Registration Flow
Test Case 1.1: New User Registration (Happy Path)
Goal: Verify new user can register with valid email, password, and name.
Precondition:
- No existing account with email
testuser-e2e-001@localhost.test - Mobile app is at login screen (or onboarding)
Steps:
On iOS simulator:
- Tap "Register" or "Sign Up" button
- Fill in:
- Email:
testuser-e2e-001@localhost.test - Password:
Test@12345 - Display Name:
E2E Test User
- Email:
- Tap "Register"
Verify response:
- Modal/toast appears: "Registration successful" or similar
- User is either:
- Auto-logged in (app goes to Onboarding screen), or
- Redirected to Login screen (if email confirmation required)
Check backend:
# Query user in database sqlite3 ~/.sentencestudio/sentencestudio.db \ "SELECT Id, Email, DisplayName, EmailConfirmed FROM AspNetUsers WHERE Email = 'testuser-e2e-001@localhost.test';"Expected output: 1 row with EmailConfirmed = 1 (in dev mode, auto-confirmed)
Check Aspire logs:
- Search for "Register" or "User created" in structured logs
- Verify no auth errors in Api service logs
Expected Outcome:
- User registered in database with correct email and display name
- Email is auto-confirmed (dev environment)
- User auto-logs in and proceeds to Onboarding screen
Screenshots:
- Registration form filled
- Success confirmation
- Onboarding screen or Dashboard visible
Test Case 1.2: Registration Validation — Duplicate Email
Goal: Verify app rejects duplicate email registration.
Precondition:
- User
testuser-e2e-001@localhost.testalready exists from Test 1.1 - Mobile app at login/registration screen
Steps:
On iOS simulator:
- Tap "Register"
- Fill in:
- Email:
testuser-e2e-001@localhost.test(duplicate) - Password:
Test@54321 - Display Name:
Another User
- Email:
- Tap "Register"
Verify error handling:
- Error toast/alert appears: "Email already in use" or "User already exists"
- App remains on registration screen
- User can correct input and retry
Check logs:
# Aspire logs should show validation error # Api service logs should contain something like: # "Registration failed: Email already exists"
Expected Outcome:
- Registration rejected with clear error message
- Database shows only original user (no duplicate created)
- User can retry with different email
Test Case 1.3: Registration Validation — Weak Password
Goal: Verify password complexity validation works.
Precondition:
- Mobile app at registration screen
Steps:
On iOS simulator:
- Tap "Register"
- Fill in:
- Email:
testuser-weak-pwd@localhost.test - Password:
test(too simple: no uppercase, no number, no special char) - Display Name:
Weak Password User
- Email:
- Tap "Register"
Verify validation:
- Error message appears (inline or toast): "Password does not meet complexity requirements"
- Registration blocked
Try again with strong password:
- Enter Password:
Test@12345 - Registration should succeed
- Enter Password:
Expected Outcome:
- Weak password rejected
- Clear feedback on password requirements
- Strong password accepted
- User created in database
Test Suite 2: Login Flow
Test Case 2.1: Existing User Login (Happy Path)
Goal: Verify existing user can log in with valid credentials.
Precondition:
- User
testuser-e2e-001@localhost.testexists with passwordTest@12345(from Test 1.1) - Mobile app at login screen (fresh install or after logout)
Steps:
On iOS simulator:
- Tap email field, enter:
testuser-e2e-001@localhost.test - Tap password field, enter:
Test@12345 - Tap "Login"
- Tap email field, enter:
Verify token flow:
- App shows brief loading indicator
- User is redirected to Onboarding screen (first login) or Dashboard (returning user)
- No errors in console
Verify secure storage:
# On simulator, check if tokens are stored in SecureStorage # (This is implicit if user stays logged in after kill/relaunch)Check API logs:
- Aspire logs should show successful login and token generation
- No auth errors
Expected Outcome:
- User logged in successfully
- JWT token issued and stored
- Refresh token stored in secure storage
- User proceeds to next screen (Onboarding or Dashboard)
Screenshots:
- Login screen with credentials filled
- Loading state
- Dashboard or Onboarding screen
Test Case 2.2: Login Validation — Wrong Password
Goal: Verify login rejected with incorrect password.
Precondition:
- User
testuser-e2e-001@localhost.testexists - Mobile app at login screen
Steps:
On iOS simulator:
- Enter Email:
testuser-e2e-001@localhost.test - Enter Password:
WrongPassword123 - Tap "Login"
- Enter Email:
Verify error handling:
- Error toast/alert: "Invalid email or password"
- App remains on login screen
- No tokens stored
- User can retry
Expected Outcome:
- Login rejected with generic error (no user enumeration)
- No tokens in secure storage
- User can try again
Test Case 2.3: Login Validation — Non-existent Email
Goal: Verify login rejected for non-existent email.
Precondition:
- Mobile app at login screen
Steps:
On iOS simulator:
- Enter Email:
nonexistent@localhost.test - Enter Password:
Test@12345 - Tap "Login"
- Enter Email:
Verify error:
- Error: "Invalid email or password" (same generic message as wrong password)
- No tokens stored
Expected Outcome:
- Login rejected
- Generic error prevents user enumeration
- No tokens in secure storage
Test Suite 3: Onboarding Flow
Test Case 3.1: First-Time Onboarding (After Registration/First Login)
Goal: Verify new user is directed to onboarding and can complete language setup.
Precondition:
- Fresh user just registered (Test 1.1) or just logged in for first time (Test 2.1)
- User is on Onboarding screen
Steps:
On iOS simulator, verify onboarding screen shows:
- Native Language selector (e.g., English)
- Target Language selector (e.g., Korean)
- Proficiency selector (e.g., Beginner)
- "Continue" or "Start Learning" button
Fill in onboarding:
- Select Native Language: English
- Select Target Language: Korean
- Select Proficiency: Beginner
- Tap "Continue"
Verify starter content creation:
- Brief loading indicator
- App transitions to Dashboard
- Dashboard shows content (vocabulary, sentences, or activities)
Verify database state:
# Check UserProfile was updated with languages sqlite3 ~/.sentencestudio/sentencestudio.db \ "SELECT Id, Email, NativeLanguage, TargetLanguage FROM UserProfiles WHERE Email = 'testuser-e2e-001@localhost.test';"Expected: 1 row with NativeLanguage = 'English', TargetLanguage = 'Korean'
Check logs for starter content creation:
- Aspire logs should show content generation steps
- No errors
Expected Outcome:
- User profile updated with language preferences
- Starter content created
- User proceeds to Dashboard
- Can view initial content (vocabulary, sentences)
Screenshots:
- Onboarding form
- Loading state
- Dashboard with content
Test Case 3.2: Returning User Skips Onboarding
Goal: Verify returning user who already completed onboarding skips to Dashboard.
Precondition:
- User completed onboarding in Test 3.1
- Kill and relaunch app (or log out and log back in with same user)
Steps:
On iOS simulator:
- Kill app (long press home, swipe up)
- Relaunch app from home screen
Verify app state:
- App restores session from SecureStorage tokens
- User goes directly to Dashboard (no login prompt, no onboarding)
- Content is visible
Verify no network calls for auth:
- Check Aspire logs
- Should see no /api/auth/login call if token is still valid
Expected Outcome:
- User auto-logged in via stored token
- Onboarding skipped
- Dashboard visible immediately
Screenshots:
- App launch
- Dashboard (no login or onboarding screens)
Test Suite 4: Token Persistence & Security
Test Case 4.1: Token Stored in SecureStorage (Kill & Relaunch)
Goal: Verify JWT token persists in platform-native secure storage across app kills.
Precondition:
- User logged in (from Test 2.1 or after onboarding)
- Dashboard is visible
- Session is fresh (token not expired)
Steps:
On iOS simulator:
- Note the currently logged-in user email in UI
- Kill app (long press home, swipe up)
- Wait 2 seconds
- Relaunch app by tapping app icon
Verify session restore:
- App boots and goes directly to Dashboard
- No login screen
- User email visible in profile (same user)
- UI is responsive
Verify in code/logs:
- Aspire logs should show no /api/auth/login call
- IdentityAuthService.SignInAsync() (silent mode) should succeed
- Token was loaded from SecureStorage
Expected Outcome:
- User token persists in secure storage
- App auto-logs in on relaunch
- No user visible interruption
Screenshots:
- Dashboard before kill
- Dashboard after relaunch (same user visible)
Test Case 4.2: Token Cleared on Logout
Goal: Verify tokens are removed from secure storage when user logs out.
Precondition:
- User logged in (Dashboard visible)
Steps:
On iOS simulator:
- Navigate to Profile or Settings (where logout button is)
- Tap "Logout" or "Sign Out"
- Confirm logout if prompted
Verify logout behavior:
- App redirects to login screen
- User email field is empty
- No cached user info visible
Kill and relaunch app:
- App boots to login screen (not Dashboard)
- SignInAsync() (silent mode) returns null (no stored token)
Verify logs:
- IdentityAuthService.SignOutAsync() clears JwtKey, RefreshKey, ExpiresKey
- No tokens in SecureStorage
Expected Outcome:
- Tokens cleared from secure storage on logout
- App boots to login screen on next launch
- Cannot access Dashboard without re-authenticating
Screenshots:
- Dashboard with logout button visible
- Login screen after logout
- Login screen after kill/relaunch
Test Suite 5: Token Refresh & Expiry
Test Case 5.1: Token Auto-Refresh (Before Expiry)
Goal: Verify expired access token is refreshed using refresh token without user intervention.
Precondition:
- User logged in with valid tokens (from Test 2.1)
- JWT access token is valid for ~15 minutes (default)
Steps:
On iOS simulator:
- Dashboard is visible
- Make an API call (e.g., navigate to Activity, fetch vocabulary list)
Monitor token state:
- First API call should use cached token (no refresh call)
- In IdentityAuthService:
_cachedTokenis valid if_cachedExpires > DateTimeOffset.UtcNow + 60s
Simulate token near-expiry (for testing):
- This is hard to test without mocking time; skip for manual E2E unless mock server available
Verify refresh flow works:
- If cached token is expired (but refresh token valid):
- GetAccessTokenAsync() calls RefreshTokenAsync()
- POST /api/auth/refresh with refresh token
- New JWT returned
- New tokens stored in SecureStorage
- API call succeeds with new token
- If cached token is expired (but refresh token valid):
Check logs:
# Aspire logs should show refresh token endpoint hit # Logs should contain: "Token refresh returned 200" or similar
Expected Outcome:
- Token refresh works transparently
- No user-visible interruption
- API calls succeed across token boundary
- New tokens stored securely
Note: Full testing requires clock manipulation or mock time; manual spot-check via logs sufficient for E2E.
Test Case 5.2: Refresh Token Expiry (7-day Boundary)
Goal: Verify app handles expired refresh token (e.g., 8 days after login).
Precondition:
- User logged in (but this test requires simulated time passage; practical approach: check token expiry in database)
Steps:
Verify refresh token lifespan in database:
# Query RefreshTokens table sqlite3 ~/.sentencestudio/sentencestudio.db \ "SELECT Token, ExpiresAt, RevokedAt FROM RefreshTokens WHERE UserId IN (SELECT Id FROM AspNetUsers WHERE Email = 'testuser-e2e-001@localhost.test') ORDER BY CreatedAt DESC LIMIT 5;"Expected: Most recent token has ExpiresAt ~7 days from CreatedAt
Simulate refresh token expiry (via database):
- If you have test fixture control, update ExpiresAt to past date:
UPDATE RefreshTokens SET ExpiresAt = datetime('now', '-1 day') WHERE Token = '<expired-token>';
- If you have test fixture control, update ExpiresAt to past date:
On iOS simulator:
- Kill and relaunch app (or wait until silent sign-in is attempted)
- App should redirect to login screen (refresh token expired)
- Display error or simply redirect with no error
Expected Outcome:
- Expired refresh token is rejected
- User forced to re-authenticate
- App goes to login screen
- Clear path to log back in
Test Suite 6: Logout Flow
Test Case 6.1: User Logout (UI)
Goal: Verify user can log out via UI and session is cleared.
Precondition:
- User logged in (Dashboard visible)
Steps:
On iOS simulator:
- Navigate to Profile or Settings screen
- Find "Logout" or "Sign Out" button
- Tap it
Verify logout behavior:
- App may show confirmation modal: "Are you sure you want to log out?"
- Tap "Yes" or "Confirm"
- App redirects to login screen
- User email is cleared from UI
Verify token cleanup:
- Check logs: IdentityAuthService.SignOutAsync() called
- Tokens removed from SecureStorage
Kill and relaunch to verify persistence:
- App boots to login screen (no auto-login)
- No tokens in storage
Expected Outcome:
- User logged out cleanly
- Tokens cleared
- App boots to login screen on relaunch
- No data loss (user profile still in database)
Screenshots:
- Dashboard with logout button
- Logout confirmation modal
- Login screen after logout
Test Suite 7: Profile Operations
Test Case 7.1: View User Profile (After Login)
Goal: Verify user can view their profile with correct name and email.
Precondition:
- User logged in (email:
testuser-e2e-001@localhost.test, display name:E2E Test User) - Dashboard or main navigation visible
Steps:
On iOS simulator:
- Navigate to Profile screen (e.g., tap avatar or "Profile" in menu)
- Verify displayed information:
- Email:
testuser-e2e-001@localhost.test - Display Name:
E2E Test User(or what was set during registration/onboarding) - Native Language: English (from onboarding)
- Target Language: Korean (from onboarding)
- Email:
Verify data accuracy:
# Check profile in database sqlite3 ~/.sentencestudio/sentencestudio.db \ "SELECT Name, Email, NativeLanguage, TargetLanguage FROM UserProfiles WHERE Email = 'testuser-e2e-001@localhost.test';"
Expected Outcome:
- Profile screen displays correct user info
- Data matches database
- No data leakage from other users
Screenshots:
- Profile screen with user info visible
Test Case 7.2: Edit Profile (Name & Email)
Goal: Verify user can edit display name and email, changes persist.
Precondition:
- User on Profile screen (from Test 7.1)
Steps:
On iOS simulator:
- Find "Edit" button or tap name field
- Change Display Name:
E2E Test User Updated - Change Email (optional):
testuser-e2e-001-updated@localhost.test - Tap "Save"
Verify save behavior:
- Loading indicator appears
- Toast/confirmation: "Profile updated successfully"
- Profile screen refreshes with new data
Verify database persistence:
sqlite3 ~/.sentencestudio/sentencestudio.db \ "SELECT Name, Email FROM UserProfiles WHERE Id IN (SELECT UserProfileId FROM AspNetUsers WHERE Email LIKE 'testuser-e2e-001%') LIMIT 1;"Expected: Updated display name is present
Kill and relaunch app:
- Log back in (if email changed)
- Profile shows updated information
Expected Outcome:
- Profile edits saved to database
- Changes visible immediately
- Changes persist across sessions
- No data loss
Screenshots:
- Profile edit form with changes
- Success toast
- Updated profile display
Test Case 7.3: Delete Account
Goal: Verify user can delete their account; data is removed, app logs out.
Precondition:
- User on Profile screen (can be a temporary test user created for this test)
- Email:
testuser-delete@localhost.test(created fresh for this test)
Steps:
On iOS simulator:
- On Profile screen, find "Delete Account" button
- Tap it
- Confirmation modal appears: "Are you sure? This cannot be undone."
- Tap "Yes, Delete My Account"
Verify deletion behavior:
- Loading indicator
- Toast: "Account deleted successfully"
- App redirects to login screen
- User is logged out
Verify database state:
# User should be deleted sqlite3 ~/.sentencestudio/sentencestudio.db \ "SELECT Email FROM AspNetUsers WHERE Email = 'testuser-delete@localhost.test';"Expected: 0 rows (user deleted)
# Associated profile should be deleted or orphaned sqlite3 ~/.sentencestudio/sentencestudio.db \ "SELECT Email FROM UserProfiles WHERE Email = 'testuser-delete@localhost.test';"Try to log back in:
- Enter deleted email and any password
- Login fails: "Invalid email or password"
Expected Outcome:
- User account deleted from database
- Associated profile deleted
- App logs out user
- Deleted email cannot log back in
- No orphaned data
Screenshots:
- Profile with delete button
- Confirmation modal
- Login screen after deletion
- (Backend verification of deletion)
Test Suite 8: Data Isolation (User A vs User B)
Test Case 8.1: Data Isolation — Vocabulary/Content Not Shared
Goal: Verify User A's vocabulary and content is not visible to User B.
Precondition:
- Create two users:
- User A:
testuser-a@localhost.test(password:TestA@12345) - User B:
testuser-b@localhost.test(password:TestB@12345)
- User A:
- Both completed onboarding with different target languages (User A: Korean, User B: Spanish)
Steps:
Login as User A:
- On iOS, log in as
testuser-a@localhost.test - Go through onboarding (or skip if already done)
- Go to Vocabulary screen
- Note the vocabulary list (e.g., 10 items for Korean)
- On iOS, log in as
Logout and login as User B:
- Logout from profile
- Log in as
testuser-b@localhost.test - Go to Vocabulary screen
- Verify vocabulary list is different (Spanish content, different count)
Verify in database:
# Get User A's content sqlite3 ~/.sentencestudio/sentencestudio.db \ "SELECT COUNT(*) as UserAVocab FROM Vocabularies WHERE UserProfileId IN (SELECT Id FROM UserProfiles WHERE Email = 'testuser-a@localhost.test');" # Get User B's content sqlite3 ~/.sentencestudio/sentencestudio.db \ "SELECT COUNT(*) as UserBVocab FROM Vocabularies WHERE UserProfileId IN (SELECT Id FROM UserProfiles WHERE Email = 'testuser-b@localhost.test');"Expected: Different counts or no overlap
Check API logs:
- Ensure API calls are filtering by user (tenant isolation)
- Aspire logs should show TenantContext.CurrentUserId being set correctly per request
Expected Outcome:
- Each user sees only their own content
- No data leakage between users
- Database queries correctly filter by user
Screenshots:
- User A's vocabulary list
- User B's different vocabulary list
- Profile info for each user (different email, language)
Test Case 8.2: JWT Claim Verification (UserId in Token)
Goal: Verify JWT token contains correct user ID and app uses it for API calls.
Precondition:
- User logged in as
testuser-a@localhost.test
Steps:
Decode JWT token (manual verification):
- Get the JWT from SecureStorage (can be logged in debug mode, or extract from API call)
- Use an online JWT decoder (https://jwt.io/) or decode in code:
var handler = new JwtSecurityTokenHandler(); var token = handler.ReadJwtToken(accessToken); var userIdClaim = token.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
Verify claims in token:
sub(subject/user ID) matches user's ApplicationUser.Idemailclaim matches emailnameclaim matches display nameexp(expiration) is reasonable (15 min from now)
Check API calls in Aspire logs:
- Aspire logs should show extracted user ID from token
- TenantContext.CurrentUserId should match token's user ID
Expected Outcome:
- JWT contains correct user ID and claims
- User ID in token matches database user
- API uses token claims to enforce data isolation
Note: This is a backend verification test; inspect logs and token structure.
Test Suite 9: Error Handling & Network Resilience
Test Case 9.1: API Server Down (Connection Refused)
Goal: Verify app handles unreachable API gracefully.
Precondition:
- User at login screen
- Dev tunnel or API server is accessible (normal state)
Steps:
Disable dev tunnel or kill API server:
- Stop Aspire AppHost (Ctrl+C or via dashboard)
- Or disconnect dev tunnel
- Verify API is unreachable:
curl -s https://c60qm31n-7012.use.devtunnels.ms/health -w "\nStatus: %{http_code}\n" # Should fail or timeout
On iOS simulator:
- Try to log in (email and password filled)
- Tap "Login"
Verify error handling:
- Brief loading indicator
- Toast/alert: "Unable to connect to server" or "Connection failed"
- User remains on login screen (not stuck)
- Can retry or close alert
Bring API back online:
- Restart Aspire AppHost
- Wait for health check to pass
- On iOS, user can tap "Login" again
- Should succeed this time
Expected Outcome:
- Graceful error message (not crash)
- User can retry
- App recovers when API comes back
Screenshots:
- Error alert/toast
- Login screen still usable
Test Case 9.2: Network Timeout (Slow Connection)
Goal: Verify app handles slow/timeout API response gracefully.
Precondition:
- API is reachable but slow
Steps:
Simulate slow API (using network throttling or server delay):
- On iOS simulator, go to Settings > Developer > Network Link Conditioner
- Or via Xcode: Simulate slow network
- Or add a delay to API auth endpoint (for testing)
On iOS simulator:
- Try to log in
- Observe loading indicator
- After 10-30 seconds, timeout occurs
Verify timeout handling:
- Loading indicator is dismissed
- Error toast: "Request timed out" or "Please try again"
- User on login screen, can retry
Restore normal network:
- Disable network throttling
- Login should succeed on retry
Expected Outcome:
- Timeout is handled (not infinite loading)
- Clear error message
- User can retry
Test Case 9.3: Invalid/Malformed API Response
Goal: Verify app handles unexpected API response gracefully.
Precondition:
- Mock API or add conditional error response for testing
Steps:
Simulate malformed response (backend test):
- Temporarily modify API to return invalid JSON or wrong schema
- E.g., return
{ "error": "..." }instead of expected AuthResponse shape
On iOS simulator:
- Try to log in
- Observe loading indicator
Verify error handling:
- App detects deserialization failure
- Toast: "An error occurred" or similar generic message
- No crash
- User on login screen
Fix API response:
- Restore correct response format
- Login should work on retry
Expected Outcome:
- Graceful handling of malformed responses
- No app crash
- User can retry
Note: This test is optional; requires controlled backend mutation.
Test Suite 10: Webapp Regression Testing
Test Case 10.1: Webapp Login Still Works
Goal: Verify webapp (Blazor Server) auth still works after mobile auth changes.
Precondition:
- Webapp is running at
https://localhost:7200(or configured URL) - User
testuser-e2e-001@localhost.testexists with passwordTest@12345
Steps:
Open browser (Playwright):
# Use Playwright or manual browser testing # Navigate to https://localhost:7200On webapp:
- Login screen visible (with email/password fields)
- Enter email:
testuser-e2e-001@localhost.test - Enter password:
Test@12345 - Click "Login"
Verify login behavior:
- Brief loading indicator
- Redirected to onboarding or dashboard
- User info visible in header (email or display name)
Verify session:
- Refresh page
- Still logged in (session persists via Redis distributed cache)
- No redirect to login
Check logs:
- Aspire logs show successful login
- No API auth errors
Expected Outcome:
- Webapp login works
- Session persists across requests
- Token acquired and cached (Redis)
Playwright Script:
// e2e-testing-workspace/tests/auth-webapp.spec.ts
import { test, expect } from '@playwright/test';
test('webapp login flow', async ({ page }) => {
await page.goto('https://localhost:7200');
await page.fill('input[name="email"]', 'testuser-e2e-001@localhost.test');
await page.fill('input[name="password"]', 'Test@12345');
await page.click('button:has-text("Login")');
await page.waitForNavigation();
await expect(page).toHaveURL(/\/(dashboard|onboarding)/);
// Verify user info visible
await expect(page.locator('text=E2E Test User')).toBeVisible();
});
Test Case 10.2: Webapp Logout Clears Session
Goal: Verify webapp logout clears session and redirects to login.
Precondition:
- Logged into webapp (from Test 10.1)
Steps:
On webapp:
- Navigate to profile or find logout button
- Click "Logout" or "Sign Out"
Verify logout:
- Redirected to login screen
- No cached session (can't navigate back to protected page)
Try to access dashboard directly:
- Navigate to
/dashboardURL directly - Redirected to login screen
- Navigate to
Expected Outcome:
- Webapp logout works
- Session cleared
- Redirects to login on next access
Test Case 10.3: Webapp Registration Still Works
Goal: Verify webapp registration endpoint works alongside mobile auth.
Precondition:
- Webapp is running
Steps:
On webapp:
- Navigate to registration page
- Fill in:
- Email:
testuser-webapp@localhost.test - Password:
TestWebApp@12345 - Display Name:
Webapp Test User
- Email:
- Click "Register"
Verify registration:
- Success message or auto-login
- User created in database
- Can log in with new credentials
Verify on mobile:
- On iOS simulator, try to log in as the same user
- Should succeed (same backend)
Expected Outcome:
- Webapp registration works
- User accessible from mobile too (shared database)
- Cross-platform compatibility
Test Suite 11: Load & Stress (Optional, Advanced)
Test Case 11.1: Concurrent Login Attempts
Goal: Verify system handles multiple simultaneous login attempts.
Precondition:
- Use load testing tool (JMeter, k6) or mobile device farm
Steps:
Simulate 10 concurrent logins:
- Same user on 10 simulator instances
- Or: 10 different users logging in simultaneously
Verify system stability:
- All logins succeed within 5 seconds
- Database locks don't occur
- All users get unique tokens
- No 500 errors in API logs
Check API performance:
- Aspire logs for latency
- No auth bottlenecks
Expected Outcome:
- System handles concurrent logins
- Response time remains acceptable (<3s per login)
- No data corruption
Note: Optional for manual E2E; recommended for performance testing phase.
Summary Checklist
Registration:
- Test 1.1: New user registration (happy path)
- Test 1.2: Duplicate email validation
- Test 1.3: Password complexity validation
Login:
- Test 2.1: Existing user login (happy path)
- Test 2.2: Wrong password rejection
- Test 2.3: Non-existent email handling
Onboarding:
- Test 3.1: First-time onboarding flow
- Test 3.2: Returning user skips onboarding
Token Persistence:
- Test 4.1: Token stored in SecureStorage
- Test 4.2: Token cleared on logout
Token Management:
- Test 5.1: Token auto-refresh (spot-check)
- Test 5.2: Refresh token expiry handling
Logout:
- Test 6.1: User logout clears tokens
Profile:
- Test 7.1: View profile
- Test 7.2: Edit profile
- Test 7.3: Delete account
Data Isolation:
- Test 8.1: User A's data not visible to User B
- Test 8.2: JWT claims verification
Error Handling:
- Test 9.1: API server down handling
- Test 9.2: Network timeout handling
- Test 9.3: Malformed response handling
Webapp Regression:
- Test 10.1: Webapp login still works
- Test 10.2: Webapp logout clears session
- Test 10.3: Webapp registration works
Load Testing (Optional):
- Test 11.1: Concurrent login attempts
Tools & Shortcuts
Simulator Management:
# Boot iOS simulator
xcrun simctl boot "iPhone 17 Pro"
# Shutdown
xcrun simctl shutdown "iPhone 17 Pro"
# View console logs
xcrun simctl spawn booted log stream --level debug --predicate 'process == "SentenceStudio"'
Database Queries (Quick Reference):
# Open database
sqlite3 ~/.sentencestudio/sentencestudio.db
# List tables
.tables
# User count
SELECT COUNT(*) FROM AspNetUsers;
# Tokens by user
SELECT Email, COUNT(*) as TokenCount FROM AspNetUsers
LEFT JOIN RefreshTokens ON AspNetUsers.Id = RefreshTokens.UserId
WHERE RefreshTokens.RevokedAt IS NULL
GROUP BY AspNetUsers.Id;
# Recent login attempts (from logs)
# (Check Aspire dashboard structured logs for "LoginRequest" events)
Aspire Logs (Structured):
# Query from Aspire dashboard
# https://localhost:8080 → Structured Logs
# Filter by resource: "Api"
# Search: "Login" or "Register" or "auth"
# Or via API (if available):
curl -s http://localhost:8080/api/resources/Api/logs?filter=Login | jq .
Screenshots (Playwright/maui-devflow):
# On iOS simulator (via maui-devflow or Appium)
# Tap screenshot button or programmatic capture in test
Known Issues & Workarounds
Token Expiry Testing:
- Hard to test without mocking system time
- Workaround: Check token expiry in database or decode JWT
- Refresh flow can be verified via logs without full expiry
SecureStorage Inspection:
- Can't directly inspect iOS SecureStorage from test harness
- Workaround: Test indirectly via app behavior (kill/relaunch)
Network Simulation:
- Simulator network is same as host machine
- Use Xcode's network throttling or external tool (Charles Proxy, Fiddler)
Concurrent Testing:
- Single simulator can't test concurrent logins
- Use device farm or multiple simulators (resource-intensive)
Next Steps
- Execute tests sequentially from Test Suite 1 onwards
- Document results in spreadsheet (test case, status, notes, screenshot links)
- Report blockers to Zoe (Lead) immediately if any critical failures
- Regression suite (Test Suite 10) must pass before each mobile/webapp release
- Archive logs from Aspire for failed test cases
Version: 1.0
Last Updated: 2026-03-14
Author: Zoe (Lead)
Maintainer: Jayne (Tester)