Fix post-registration navigation and add comprehensive registration UI tests
- Fix RegisterView to call AuthenticationManager.login() after email verification so user is properly transitioned to home screen instead of returning to login - Fix ResidencesListView to load data when authentication state becomes true, ensuring residences load after registration/login - Add accessibility identifier to verification code field for UI testing - Add NSAppTransportSecurity exceptions for localhost/127.0.0.1 for local dev - Add comprehensive XCUITest suite for registration flow including: - Form validation tests (empty fields, invalid email, mismatched passwords) - Full registration and verification flow test - Logout from verification screen test - Helper scripts for test user cleanup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
600
iosApp/MyCribUITests/RegistrationTests.swift
Normal file
600
iosApp/MyCribUITests/RegistrationTests.swift
Normal file
@@ -0,0 +1,600 @@
|
|||||||
|
import XCTest
|
||||||
|
#if os(macOS)
|
||||||
|
import Foundation
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// Comprehensive registration flow tests
|
||||||
|
/// Tests the complete registration and email verification flow
|
||||||
|
///
|
||||||
|
/// NOTE: Tests that require database access (fetchVerificationCode, cleanupTestUser)
|
||||||
|
/// use shell scripts that run on the host machine. These tests are designed to run
|
||||||
|
/// in the iOS Simulator with the backend running locally.
|
||||||
|
final class RegistrationTests: XCTestCase {
|
||||||
|
var app: XCUIApplication!
|
||||||
|
|
||||||
|
// Test user credentials - using timestamp to ensure unique users
|
||||||
|
private var testUsername: String {
|
||||||
|
return "testuser_\(Int(Date().timeIntervalSince1970))"
|
||||||
|
}
|
||||||
|
private var testEmail: String {
|
||||||
|
return "test_\(Int(Date().timeIntervalSince1970))@example.com"
|
||||||
|
}
|
||||||
|
private let testPassword = "TestPass123!"
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
app = XCUIApplication()
|
||||||
|
app.launch()
|
||||||
|
ensureLoggedOut()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDownWithError() throws {
|
||||||
|
// Clean up: logout if logged in
|
||||||
|
ensureLoggedOut()
|
||||||
|
app = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helper Methods
|
||||||
|
|
||||||
|
private func ensureLoggedOut() {
|
||||||
|
UITestHelpers.ensureLoggedOut(app: app)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func navigateToRegistration() {
|
||||||
|
let signUpButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign Up'")).firstMatch
|
||||||
|
XCTAssertTrue(signUpButton.waitForExistence(timeout: 5), "Sign Up button should exist on login screen")
|
||||||
|
signUpButton.tap()
|
||||||
|
sleep(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dismisses the iOS Strong Password suggestion overlay if it appears
|
||||||
|
private func dismissStrongPasswordSuggestion() {
|
||||||
|
// Look for "Choose My Own Password" or similar button in the password suggestion
|
||||||
|
let chooseOwnPassword = app.buttons["Choose My Own Password"]
|
||||||
|
if chooseOwnPassword.waitForExistence(timeout: 2) {
|
||||||
|
chooseOwnPassword.tap()
|
||||||
|
sleep(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try alternate labels
|
||||||
|
let notNowButton = app.buttons["Not Now"]
|
||||||
|
if notNowButton.exists {
|
||||||
|
notNowButton.tap()
|
||||||
|
sleep(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try tapping outside the suggestion to dismiss it
|
||||||
|
let strongPasswordText = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'Strong Password'")).firstMatch
|
||||||
|
if strongPasswordText.exists {
|
||||||
|
// Tap somewhere else to dismiss
|
||||||
|
app.tap()
|
||||||
|
sleep(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fixed test verification code - Django uses this code for emails starting with "test_" in DEBUG mode
|
||||||
|
/// This matches ConfirmationCode.TEST_VERIFICATION_CODE in the Django backend
|
||||||
|
private let testVerificationCode = "123456"
|
||||||
|
|
||||||
|
/// Note: cleanupTestUser should be called from command line after tests complete
|
||||||
|
/// Run: cd /Users/treyt/Desktop/code/MyCrib/myCribAPI && python manage.py shell -c "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.filter(email__startswith='test_').delete()"
|
||||||
|
private func cleanupTestUser(email: String) {
|
||||||
|
print("Cleanup test user: \(email)")
|
||||||
|
print("Run manually if needed: cd /Users/treyt/Desktop/code/MyCrib/myCribAPI && python manage.py shell -c \"from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.filter(email='\(email)').delete()\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Registration Form Tests
|
||||||
|
|
||||||
|
func testRegistrationScreenElements() {
|
||||||
|
// Given: User is on login screen
|
||||||
|
navigateToRegistration()
|
||||||
|
|
||||||
|
// Then: Registration form should have all expected elements
|
||||||
|
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||||
|
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
|
||||||
|
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
|
||||||
|
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
|
||||||
|
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||||
|
let cancelButton = app.buttons[AccessibilityIdentifiers.Authentication.registerCancelButton]
|
||||||
|
|
||||||
|
XCTAssertTrue(usernameField.waitForExistence(timeout: 5), "Username field should exist")
|
||||||
|
XCTAssertTrue(emailField.exists, "Email field should exist")
|
||||||
|
XCTAssertTrue(passwordField.exists, "Password field should exist")
|
||||||
|
XCTAssertTrue(confirmPasswordField.exists, "Confirm password field should exist")
|
||||||
|
XCTAssertTrue(createAccountButton.exists, "Create Account button should exist")
|
||||||
|
XCTAssertTrue(cancelButton.exists, "Cancel button should exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRegistrationWithEmptyFields() {
|
||||||
|
// Given: User is on registration screen
|
||||||
|
navigateToRegistration()
|
||||||
|
|
||||||
|
// When: User taps Create Account without filling fields
|
||||||
|
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||||
|
XCTAssertTrue(createAccountButton.waitForExistence(timeout: 5))
|
||||||
|
createAccountButton.tap()
|
||||||
|
|
||||||
|
// Then: Error message should appear
|
||||||
|
sleep(2)
|
||||||
|
let errorExists = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'required' OR label CONTAINS[c] 'error' OR label CONTAINS[c] 'invalid'")).firstMatch.waitForExistence(timeout: 3)
|
||||||
|
XCTAssertTrue(errorExists, "Error message should appear for empty fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRegistrationWithInvalidEmail() {
|
||||||
|
// Given: User is on registration screen
|
||||||
|
navigateToRegistration()
|
||||||
|
|
||||||
|
// When: User fills form with invalid email
|
||||||
|
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||||
|
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
|
||||||
|
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
|
||||||
|
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
|
||||||
|
|
||||||
|
XCTAssertTrue(usernameField.waitForExistence(timeout: 5))
|
||||||
|
|
||||||
|
usernameField.tap()
|
||||||
|
usernameField.typeText("testuser")
|
||||||
|
|
||||||
|
emailField.tap()
|
||||||
|
emailField.typeText("invalid-email") // Invalid email format
|
||||||
|
|
||||||
|
passwordField.tap()
|
||||||
|
passwordField.typeText(testPassword)
|
||||||
|
|
||||||
|
confirmPasswordField.tap()
|
||||||
|
confirmPasswordField.typeText(testPassword)
|
||||||
|
|
||||||
|
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||||
|
createAccountButton.tap()
|
||||||
|
|
||||||
|
// Then: Error message for invalid email should appear
|
||||||
|
sleep(2)
|
||||||
|
let errorExists = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'email' OR label CONTAINS[c] 'invalid'")).firstMatch.waitForExistence(timeout: 3)
|
||||||
|
XCTAssertTrue(errorExists, "Error message should appear for invalid email")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRegistrationWithMismatchedPasswords() {
|
||||||
|
// Given: User is on registration screen
|
||||||
|
navigateToRegistration()
|
||||||
|
|
||||||
|
// When: User fills form with mismatched passwords
|
||||||
|
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||||
|
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
|
||||||
|
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
|
||||||
|
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
|
||||||
|
|
||||||
|
XCTAssertTrue(usernameField.waitForExistence(timeout: 5))
|
||||||
|
|
||||||
|
usernameField.tap()
|
||||||
|
usernameField.typeText("testuser")
|
||||||
|
|
||||||
|
emailField.tap()
|
||||||
|
emailField.typeText("test@example.com")
|
||||||
|
|
||||||
|
passwordField.tap()
|
||||||
|
passwordField.typeText("Password123!")
|
||||||
|
|
||||||
|
confirmPasswordField.tap()
|
||||||
|
confirmPasswordField.typeText("DifferentPassword123!") // Mismatched password
|
||||||
|
|
||||||
|
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||||
|
createAccountButton.tap()
|
||||||
|
|
||||||
|
// Then: Error message for mismatched passwords should appear
|
||||||
|
sleep(2)
|
||||||
|
let errorExists = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'match' OR label CONTAINS[c] 'password'")).firstMatch.waitForExistence(timeout: 3)
|
||||||
|
XCTAssertTrue(errorExists, "Error message should appear for mismatched passwords")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRegistrationWithWeakPassword() {
|
||||||
|
// Given: User is on registration screen
|
||||||
|
navigateToRegistration()
|
||||||
|
|
||||||
|
// When: User fills form with weak password
|
||||||
|
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||||
|
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
|
||||||
|
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
|
||||||
|
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
|
||||||
|
|
||||||
|
XCTAssertTrue(usernameField.waitForExistence(timeout: 5))
|
||||||
|
|
||||||
|
usernameField.tap()
|
||||||
|
usernameField.typeText("testuser")
|
||||||
|
|
||||||
|
emailField.tap()
|
||||||
|
emailField.typeText("test@example.com")
|
||||||
|
|
||||||
|
passwordField.tap()
|
||||||
|
passwordField.typeText("weak") // Too short/weak password
|
||||||
|
|
||||||
|
confirmPasswordField.tap()
|
||||||
|
confirmPasswordField.typeText("weak")
|
||||||
|
|
||||||
|
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||||
|
createAccountButton.tap()
|
||||||
|
|
||||||
|
// Then: Error message for weak password should appear
|
||||||
|
sleep(2)
|
||||||
|
let errorExists = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'password' OR label CONTAINS[c] 'character' OR label CONTAINS[c] 'strong'")).firstMatch.waitForExistence(timeout: 3)
|
||||||
|
XCTAssertTrue(errorExists, "Error message should appear for weak password")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCancelRegistration() {
|
||||||
|
// Given: User is on registration screen
|
||||||
|
navigateToRegistration()
|
||||||
|
|
||||||
|
// When: User taps Cancel button
|
||||||
|
let cancelButton = app.buttons[AccessibilityIdentifiers.Authentication.registerCancelButton]
|
||||||
|
XCTAssertTrue(cancelButton.waitForExistence(timeout: 5))
|
||||||
|
cancelButton.tap()
|
||||||
|
|
||||||
|
// Then: Should return to login screen
|
||||||
|
sleep(1)
|
||||||
|
let welcomeText = app.staticTexts["Welcome Back"]
|
||||||
|
XCTAssertTrue(welcomeText.waitForExistence(timeout: 5), "Should return to login screen")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Full Registration Flow Tests
|
||||||
|
|
||||||
|
func testSuccessfulRegistrationAndVerification() {
|
||||||
|
// Use unique credentials for this test
|
||||||
|
let username = testUsername
|
||||||
|
let email = testEmail
|
||||||
|
|
||||||
|
// Given: User is on registration screen
|
||||||
|
navigateToRegistration()
|
||||||
|
|
||||||
|
// When: User fills in valid registration details
|
||||||
|
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||||
|
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
|
||||||
|
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
|
||||||
|
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
|
||||||
|
|
||||||
|
XCTAssertTrue(usernameField.waitForExistence(timeout: 5))
|
||||||
|
|
||||||
|
usernameField.tap()
|
||||||
|
usernameField.typeText(username)
|
||||||
|
|
||||||
|
emailField.tap()
|
||||||
|
emailField.typeText(email)
|
||||||
|
|
||||||
|
// Handle strong password suggestion by dismissing it
|
||||||
|
passwordField.tap()
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Dismiss the strong password suggestion if it appears
|
||||||
|
dismissStrongPasswordSuggestion()
|
||||||
|
|
||||||
|
// Now type the password
|
||||||
|
passwordField.tap()
|
||||||
|
passwordField.typeText(testPassword)
|
||||||
|
|
||||||
|
confirmPasswordField.tap()
|
||||||
|
sleep(1)
|
||||||
|
dismissStrongPasswordSuggestion()
|
||||||
|
confirmPasswordField.tap()
|
||||||
|
confirmPasswordField.typeText(testPassword)
|
||||||
|
|
||||||
|
// Submit registration
|
||||||
|
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||||
|
createAccountButton.tap()
|
||||||
|
|
||||||
|
// Wait for verification screen to appear
|
||||||
|
let verifyEmailTitle = app.staticTexts["Verify Your Email"]
|
||||||
|
XCTAssertTrue(verifyEmailTitle.waitForExistence(timeout: 10), "Should navigate to email verification screen")
|
||||||
|
|
||||||
|
// Use the fixed test verification code
|
||||||
|
// Django uses "123456" for emails starting with "test_" when DEBUG=True
|
||||||
|
let verificationCode = testVerificationCode
|
||||||
|
print("Using test verification code: \(verificationCode)")
|
||||||
|
|
||||||
|
// Enter the verification code
|
||||||
|
let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField]
|
||||||
|
XCTAssertTrue(codeField.waitForExistence(timeout: 5), "Verification code field should exist")
|
||||||
|
codeField.tap()
|
||||||
|
codeField.typeText(verificationCode)
|
||||||
|
|
||||||
|
// Submit verification
|
||||||
|
let verifyButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch
|
||||||
|
XCTAssertTrue(verifyButton.exists, "Verify button should exist")
|
||||||
|
verifyButton.tap()
|
||||||
|
|
||||||
|
// Then: Should navigate to main app screen (home/residences tab)
|
||||||
|
let residencesTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Residences'")).firstMatch
|
||||||
|
XCTAssertTrue(residencesTab.waitForExistence(timeout: 10), "Should navigate to main app after successful verification")
|
||||||
|
|
||||||
|
// Verify we're on the home screen by checking for residences-related content
|
||||||
|
let homeScreenVisible = residencesTab.isSelected || app.navigationBars.containing(NSPredicate(format: "identifier CONTAINS[c] 'Residences' OR identifier CONTAINS[c] 'Home' OR identifier CONTAINS[c] 'Properties'")).firstMatch.exists
|
||||||
|
XCTAssertTrue(homeScreenVisible || residencesTab.exists, "Should be on home/residences screen after registration")
|
||||||
|
|
||||||
|
// Navigate to Profile tab and log out
|
||||||
|
let profileTab = app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Profile'")).firstMatch
|
||||||
|
XCTAssertTrue(profileTab.waitForExistence(timeout: 5), "Profile tab should exist")
|
||||||
|
profileTab.tap()
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Tap logout button
|
||||||
|
let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout' OR label CONTAINS[c] 'Log Out' OR label CONTAINS[c] 'Sign Out'")).firstMatch
|
||||||
|
XCTAssertTrue(logoutButton.waitForExistence(timeout: 5), "Logout button should exist on profile screen")
|
||||||
|
logoutButton.tap()
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Confirm logout in alert if present
|
||||||
|
let alertLogoutButton = app.alerts.buttons["Log Out"]
|
||||||
|
if alertLogoutButton.waitForExistence(timeout: 3) {
|
||||||
|
alertLogoutButton.tap()
|
||||||
|
sleep(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we're back on login screen
|
||||||
|
let welcomeText = app.staticTexts["Welcome Back"]
|
||||||
|
XCTAssertTrue(welcomeText.waitForExistence(timeout: 5), "Should return to login screen after logout")
|
||||||
|
|
||||||
|
// Cleanup test user
|
||||||
|
cleanupTestUser(email: email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRegistrationWithInvalidVerificationCode() {
|
||||||
|
// Use unique credentials for this test
|
||||||
|
let username = testUsername
|
||||||
|
let email = testEmail
|
||||||
|
|
||||||
|
// Given: User is on registration screen
|
||||||
|
navigateToRegistration()
|
||||||
|
|
||||||
|
// Register a new user
|
||||||
|
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||||
|
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
|
||||||
|
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
|
||||||
|
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
|
||||||
|
|
||||||
|
XCTAssertTrue(usernameField.waitForExistence(timeout: 5))
|
||||||
|
|
||||||
|
usernameField.tap()
|
||||||
|
usernameField.typeText(username)
|
||||||
|
|
||||||
|
emailField.tap()
|
||||||
|
emailField.typeText(email)
|
||||||
|
|
||||||
|
passwordField.tap()
|
||||||
|
sleep(1)
|
||||||
|
dismissStrongPasswordSuggestion()
|
||||||
|
passwordField.tap()
|
||||||
|
passwordField.typeText(testPassword)
|
||||||
|
|
||||||
|
confirmPasswordField.tap()
|
||||||
|
sleep(1)
|
||||||
|
dismissStrongPasswordSuggestion()
|
||||||
|
confirmPasswordField.tap()
|
||||||
|
confirmPasswordField.typeText(testPassword)
|
||||||
|
|
||||||
|
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||||
|
createAccountButton.tap()
|
||||||
|
|
||||||
|
// Wait for verification screen
|
||||||
|
let verifyEmailTitle = app.staticTexts["Verify Your Email"]
|
||||||
|
XCTAssertTrue(verifyEmailTitle.waitForExistence(timeout: 10), "Should navigate to email verification screen")
|
||||||
|
|
||||||
|
// Enter an invalid verification code
|
||||||
|
let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField]
|
||||||
|
XCTAssertTrue(codeField.waitForExistence(timeout: 5))
|
||||||
|
codeField.tap()
|
||||||
|
codeField.typeText("000000") // Invalid code
|
||||||
|
|
||||||
|
// Submit verification
|
||||||
|
let verifyButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch
|
||||||
|
verifyButton.tap()
|
||||||
|
|
||||||
|
// Then: Error message should appear
|
||||||
|
sleep(3)
|
||||||
|
let errorExists = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'invalid' OR label CONTAINS[c] 'error' OR label CONTAINS[c] 'incorrect'")).firstMatch.waitForExistence(timeout: 5)
|
||||||
|
XCTAssertTrue(errorExists, "Error message should appear for invalid verification code")
|
||||||
|
|
||||||
|
// Cleanup: Logout and delete test user
|
||||||
|
let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).firstMatch
|
||||||
|
if logoutButton.exists {
|
||||||
|
logoutButton.tap()
|
||||||
|
sleep(2)
|
||||||
|
}
|
||||||
|
cleanupTestUser(email: email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLogoutFromVerificationScreen() {
|
||||||
|
// Use unique credentials for this test
|
||||||
|
let username = testUsername
|
||||||
|
let email = testEmail
|
||||||
|
|
||||||
|
// Given: User is on registration screen
|
||||||
|
navigateToRegistration()
|
||||||
|
|
||||||
|
// Register a new user
|
||||||
|
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||||
|
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
|
||||||
|
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
|
||||||
|
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
|
||||||
|
|
||||||
|
XCTAssertTrue(usernameField.waitForExistence(timeout: 5))
|
||||||
|
|
||||||
|
usernameField.tap()
|
||||||
|
usernameField.typeText(username)
|
||||||
|
|
||||||
|
emailField.tap()
|
||||||
|
emailField.typeText(email)
|
||||||
|
|
||||||
|
passwordField.tap()
|
||||||
|
sleep(1)
|
||||||
|
dismissStrongPasswordSuggestion()
|
||||||
|
passwordField.tap()
|
||||||
|
passwordField.typeText(testPassword)
|
||||||
|
|
||||||
|
confirmPasswordField.tap()
|
||||||
|
sleep(1)
|
||||||
|
dismissStrongPasswordSuggestion()
|
||||||
|
confirmPasswordField.tap()
|
||||||
|
confirmPasswordField.typeText(testPassword)
|
||||||
|
|
||||||
|
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||||
|
createAccountButton.tap()
|
||||||
|
|
||||||
|
// Wait for verification screen
|
||||||
|
let verifyEmailTitle = app.staticTexts["Verify Your Email"]
|
||||||
|
XCTAssertTrue(verifyEmailTitle.waitForExistence(timeout: 10), "Should navigate to email verification screen")
|
||||||
|
|
||||||
|
// When: User taps Logout button
|
||||||
|
let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).firstMatch
|
||||||
|
XCTAssertTrue(logoutButton.waitForExistence(timeout: 5), "Logout button should exist on verification screen")
|
||||||
|
logoutButton.tap()
|
||||||
|
|
||||||
|
// Then: Should return to login screen
|
||||||
|
sleep(2)
|
||||||
|
let welcomeText = app.staticTexts["Welcome Back"]
|
||||||
|
XCTAssertTrue(welcomeText.waitForExistence(timeout: 5), "Should return to login screen after logout")
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
cleanupTestUser(email: email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testVerificationCodeFieldValidation() {
|
||||||
|
// Use unique credentials for this test
|
||||||
|
let username = testUsername
|
||||||
|
let email = testEmail
|
||||||
|
|
||||||
|
// Given: User is on registration screen and registers
|
||||||
|
navigateToRegistration()
|
||||||
|
|
||||||
|
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||||
|
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
|
||||||
|
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
|
||||||
|
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
|
||||||
|
|
||||||
|
XCTAssertTrue(usernameField.waitForExistence(timeout: 5))
|
||||||
|
|
||||||
|
usernameField.tap()
|
||||||
|
usernameField.typeText(username)
|
||||||
|
|
||||||
|
emailField.tap()
|
||||||
|
emailField.typeText(email)
|
||||||
|
|
||||||
|
passwordField.tap()
|
||||||
|
sleep(1)
|
||||||
|
dismissStrongPasswordSuggestion()
|
||||||
|
passwordField.tap()
|
||||||
|
passwordField.typeText(testPassword)
|
||||||
|
|
||||||
|
confirmPasswordField.tap()
|
||||||
|
sleep(1)
|
||||||
|
dismissStrongPasswordSuggestion()
|
||||||
|
confirmPasswordField.tap()
|
||||||
|
confirmPasswordField.typeText(testPassword)
|
||||||
|
|
||||||
|
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||||
|
createAccountButton.tap()
|
||||||
|
|
||||||
|
// Wait for verification screen
|
||||||
|
let verifyEmailTitle = app.staticTexts["Verify Your Email"]
|
||||||
|
XCTAssertTrue(verifyEmailTitle.waitForExistence(timeout: 10))
|
||||||
|
|
||||||
|
// When: User tries to verify with incomplete code
|
||||||
|
let codeField = app.textFields[AccessibilityIdentifiers.Authentication.verificationCodeField]
|
||||||
|
XCTAssertTrue(codeField.waitForExistence(timeout: 5))
|
||||||
|
codeField.tap()
|
||||||
|
codeField.typeText("123") // Only 3 digits
|
||||||
|
|
||||||
|
// Then: Verify button should be disabled or show error
|
||||||
|
let verifyButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Verify'")).firstMatch
|
||||||
|
XCTAssertTrue(verifyButton.exists)
|
||||||
|
|
||||||
|
// The button might be disabled or tapping it shows an error
|
||||||
|
// Check that verification doesn't proceed with incomplete code
|
||||||
|
verifyButton.tap()
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Should still be on verification screen
|
||||||
|
XCTAssertTrue(verifyEmailTitle.exists, "Should still be on verification screen with incomplete code")
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
let logoutButton = app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Logout'")).firstMatch
|
||||||
|
if logoutButton.exists {
|
||||||
|
logoutButton.tap()
|
||||||
|
sleep(2)
|
||||||
|
}
|
||||||
|
cleanupTestUser(email: email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRegistrationWithExistingUsername() {
|
||||||
|
// This test requires an existing user in the database
|
||||||
|
// First, we need to ensure testuser exists
|
||||||
|
|
||||||
|
// Given: User is on registration screen
|
||||||
|
navigateToRegistration()
|
||||||
|
|
||||||
|
// When: User tries to register with existing username
|
||||||
|
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||||
|
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
|
||||||
|
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
|
||||||
|
let confirmPasswordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerConfirmPasswordField]
|
||||||
|
|
||||||
|
XCTAssertTrue(usernameField.waitForExistence(timeout: 5))
|
||||||
|
|
||||||
|
usernameField.tap()
|
||||||
|
usernameField.typeText("testuser") // Assuming this user already exists
|
||||||
|
|
||||||
|
emailField.tap()
|
||||||
|
emailField.typeText("newemail_\(Int(Date().timeIntervalSince1970))@example.com")
|
||||||
|
|
||||||
|
passwordField.tap()
|
||||||
|
passwordField.typeText(testPassword)
|
||||||
|
|
||||||
|
confirmPasswordField.tap()
|
||||||
|
confirmPasswordField.typeText(testPassword)
|
||||||
|
|
||||||
|
let createAccountButton = app.buttons[AccessibilityIdentifiers.Authentication.registerButton]
|
||||||
|
createAccountButton.tap()
|
||||||
|
|
||||||
|
// Then: Error message for existing username should appear
|
||||||
|
sleep(3)
|
||||||
|
let errorExists = app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'exists' OR label CONTAINS[c] 'already' OR label CONTAINS[c] 'taken'")).firstMatch.waitForExistence(timeout: 5)
|
||||||
|
XCTAssertTrue(errorExists, "Error message should appear for existing username")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKeyboardNavigationDuringRegistration() {
|
||||||
|
// Given: User is on registration screen
|
||||||
|
navigateToRegistration()
|
||||||
|
|
||||||
|
// When: User navigates through fields using keyboard
|
||||||
|
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.registerUsernameField]
|
||||||
|
let emailField = app.textFields[AccessibilityIdentifiers.Authentication.registerEmailField]
|
||||||
|
let passwordField = app.secureTextFields[AccessibilityIdentifiers.Authentication.registerPasswordField]
|
||||||
|
|
||||||
|
XCTAssertTrue(usernameField.waitForExistence(timeout: 5))
|
||||||
|
|
||||||
|
// Start with username field
|
||||||
|
usernameField.tap()
|
||||||
|
XCTAssertTrue(usernameField.hasKeyboardFocus, "Username field should have keyboard focus")
|
||||||
|
|
||||||
|
usernameField.typeText("testuser\n") // Press return to move to next field
|
||||||
|
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Email field should now be focused (or at least exist)
|
||||||
|
XCTAssertTrue(emailField.exists, "Email field should exist")
|
||||||
|
|
||||||
|
emailField.tap()
|
||||||
|
emailField.typeText("test@example.com\n")
|
||||||
|
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Password field should now be accessible
|
||||||
|
XCTAssertTrue(passwordField.exists, "Password field should exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - XCUIElement Extension for keyboard focus
|
||||||
|
|
||||||
|
extension XCUIElement {
|
||||||
|
var hasKeyboardFocus: Bool {
|
||||||
|
return (value(forKey: "hasKeyboardFocus") as? Bool) ?? false
|
||||||
|
}
|
||||||
|
}
|
||||||
36
iosApp/MyCribUITests/Scripts/cleanup_test_users.sh
Executable file
36
iosApp/MyCribUITests/Scripts/cleanup_test_users.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script to clean up test users from Django database
|
||||||
|
# Usage: ./cleanup_test_users.sh [email]
|
||||||
|
# If no email provided, cleans up all users with email starting with 'test_'
|
||||||
|
|
||||||
|
EMAIL="$1"
|
||||||
|
|
||||||
|
cd /Users/treyt/Desktop/code/MyCrib/myCribAPI
|
||||||
|
|
||||||
|
if [ -n "$EMAIL" ]; then
|
||||||
|
FILTER="email='$EMAIL'"
|
||||||
|
else
|
||||||
|
FILTER="email__startswith='test_'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try docker exec first (if running in Docker)
|
||||||
|
if docker ps --format '{{.Names}}' | grep -q 'mycrib-web\|myCrib-web'; then
|
||||||
|
CONTAINER_NAME=$(docker ps --format '{{.Names}}' | grep -E 'mycrib-web|myCrib-web' | head -1)
|
||||||
|
docker exec "$CONTAINER_NAME" python manage.py shell -c "
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
User = get_user_model()
|
||||||
|
deleted = User.objects.filter($FILTER).delete()
|
||||||
|
print(f'Deleted: {deleted}')
|
||||||
|
" 2>/dev/null
|
||||||
|
else
|
||||||
|
# Fallback to local Python
|
||||||
|
export DJANGO_SETTINGS_MODULE=myCrib.settings
|
||||||
|
python manage.py shell -c "
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
User = get_user_model()
|
||||||
|
deleted = User.objects.filter($FILTER).delete()
|
||||||
|
print(f'Deleted: {deleted}')
|
||||||
|
" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Test users cleanup complete"
|
||||||
57
iosApp/MyCribUITests/Scripts/get_verification_code.sh
Executable file
57
iosApp/MyCribUITests/Scripts/get_verification_code.sh
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script to fetch verification code from Django database
|
||||||
|
# Usage: ./get_verification_code.sh <email>
|
||||||
|
# Output: Writes the verification code to /tmp/mycrib_verification_code_<sanitized_email>.txt
|
||||||
|
|
||||||
|
EMAIL="$1"
|
||||||
|
|
||||||
|
if [ -z "$EMAIL" ]; then
|
||||||
|
echo "Usage: $0 <email>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sanitize email for filename
|
||||||
|
SANITIZED_EMAIL=$(echo "$EMAIL" | sed 's/@/_at_/g' | sed 's/\./_dot_/g')
|
||||||
|
OUTPUT_FILE="/tmp/mycrib_verification_code_${SANITIZED_EMAIL}.txt"
|
||||||
|
|
||||||
|
cd /Users/treyt/Desktop/code/MyCrib/myCribAPI
|
||||||
|
|
||||||
|
# Try docker exec first (if running in Docker)
|
||||||
|
if docker ps --format '{{.Names}}' | grep -q 'mycrib-web\|myCrib-web'; then
|
||||||
|
CONTAINER_NAME=$(docker ps --format '{{.Names}}' | grep -E 'mycrib-web|myCrib-web' | head -1)
|
||||||
|
CODE=$(docker exec "$CONTAINER_NAME" python manage.py shell -c "
|
||||||
|
from user.models import ConfirmationCode
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
User = get_user_model()
|
||||||
|
try:
|
||||||
|
user = User.objects.get(email='$EMAIL')
|
||||||
|
code = ConfirmationCode.objects.filter(user=user, is_used=False).latest('created_at')
|
||||||
|
print(code.code)
|
||||||
|
except Exception as e:
|
||||||
|
print('ERROR:', e)
|
||||||
|
" 2>/dev/null)
|
||||||
|
else
|
||||||
|
# Fallback to local Python
|
||||||
|
export DJANGO_SETTINGS_MODULE=myCrib.settings
|
||||||
|
CODE=$(python manage.py shell -c "
|
||||||
|
from user.models import ConfirmationCode
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
User = get_user_model()
|
||||||
|
try:
|
||||||
|
user = User.objects.get(email='$EMAIL')
|
||||||
|
code = ConfirmationCode.objects.filter(user=user, is_used=False).latest('created_at')
|
||||||
|
print(code.code)
|
||||||
|
except Exception as e:
|
||||||
|
print('ERROR:', e)
|
||||||
|
" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if we got a valid 6-digit code
|
||||||
|
if [[ "$CODE" =~ ^[0-9]{6}$ ]]; then
|
||||||
|
echo "$CODE" > "$OUTPUT_FILE"
|
||||||
|
echo "Verification code saved to $OUTPUT_FILE: $CODE"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Failed to get verification code: $CODE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -17,6 +17,24 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSExceptionDomains</key>
|
||||||
|
<dict>
|
||||||
|
<key>localhost</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>127.0.0.1</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>remote-notification</string>
|
<string>remote-notification</string>
|
||||||
|
|||||||
@@ -135,8 +135,11 @@ struct RegisterView: View {
|
|||||||
.fullScreenCover(isPresented: $viewModel.isRegistered) {
|
.fullScreenCover(isPresented: $viewModel.isRegistered) {
|
||||||
VerifyEmailView(
|
VerifyEmailView(
|
||||||
onVerifySuccess: {
|
onVerifySuccess: {
|
||||||
dismiss()
|
// User has verified their email - mark as authenticated
|
||||||
|
// This will update RootView to show the main app
|
||||||
|
AuthenticationManager.shared.login()
|
||||||
showVerifyEmail = false
|
showVerifyEmail = false
|
||||||
|
dismiss()
|
||||||
},
|
},
|
||||||
onLogout: {
|
onLogout: {
|
||||||
// Logout and return to login screen
|
// Logout and return to login screen
|
||||||
|
|||||||
@@ -106,7 +106,11 @@ struct ResidencesListView: View {
|
|||||||
.interactiveDismissDisabled()
|
.interactiveDismissDisabled()
|
||||||
}
|
}
|
||||||
.onChange(of: authManager.isAuthenticated) { isAuth in
|
.onChange(of: authManager.isAuthenticated) { isAuth in
|
||||||
if !isAuth {
|
if isAuth {
|
||||||
|
// User just logged in or registered - load their residences
|
||||||
|
viewModel.loadMyResidences()
|
||||||
|
} else {
|
||||||
|
// User logged out - clear data
|
||||||
viewModel.myResidences = nil
|
viewModel.myResidences = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ struct VerifyEmailView: View {
|
|||||||
.frame(height: 60)
|
.frame(height: 60)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.focused($isFocused)
|
.focused($isFocused)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.verificationCodeField)
|
||||||
.onChange(of: viewModel.code) { _, newValue in
|
.onChange(of: viewModel.code) { _, newValue in
|
||||||
// Limit to 6 digits
|
// Limit to 6 digits
|
||||||
if newValue.count > 6 {
|
if newValue.count > 6 {
|
||||||
|
|||||||
Reference in New Issue
Block a user