diff --git a/CLAUDE.md b/CLAUDE.md index 3822f1a..08a2111 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,12 +18,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ### Testing ```bash -# Run all tests except UI tests +# Run all tests ./gradlew test -# Run UI tests only -./gradlew uiTest - # Run a specific test class ./gradlew test --tests TestClassName @@ -58,7 +55,7 @@ This is a Spring Boot demo application showcasing the [Spring User Framework](ht 4. **Testing Strategy**: - Unit tests for individual components - Integration tests using `@IntegrationTest` annotation (combines Spring Boot test setup) - - UI tests with Selenide for end-to-end testing + - E2E tests with Playwright (in `playwright/` directory) - API tests using MockMvc for REST endpoints ### Important Conventions diff --git a/README.md b/README.md index 7ef0a52..f36d22d 100644 --- a/README.md +++ b/README.md @@ -193,12 +193,9 @@ This project includes comprehensive testing with multiple approaches: ### Running Tests ```bash -# Run all tests except UI tests +# Run all tests ./gradlew test -# Run UI tests only (requires running application) -./gradlew uiTest - # Run specific test class ./gradlew test --tests UserApiTest @@ -211,7 +208,7 @@ This project includes comprehensive testing with multiple approaches: - **Unit Tests**: Fast tests for individual components - **Integration Tests**: Tests using `@IntegrationTest` with Spring context - **API Tests**: REST endpoint testing with MockMvc -- **UI Tests**: End-to-end testing with Selenide +- **UI Tests**: End-to-end testing with Playwright - **Security Tests**: Authentication and authorization testing ### Test Data @@ -641,7 +638,7 @@ This project supports **Spring Boot DevTools** for live reload and auto-restart. | **Security** | Spring Security 7 | Authentication, authorization, CSRF protection | | **Data** | Spring Data JPA + Hibernate | Object-relational mapping and data access | | **Database** | MariaDB/MySQL | Primary data persistence | -| **Testing** | JUnit 5 + Selenide | Unit, integration, and UI testing | +| **Testing** | JUnit 5 + Playwright | Unit, integration, and UI testing | | **Build** | Gradle | Dependency management and build automation | | **Containers** | Docker + Docker Compose | Development and deployment | diff --git a/build.gradle b/build.gradle index 814cd6f..46404f2 100644 --- a/build.gradle +++ b/build.gradle @@ -39,7 +39,7 @@ repositories { dependencies { // DigitalSanctuary Spring User Framework - implementation 'com.digitalsanctuary:ds-spring-user-framework:4.3.0' + implementation 'com.digitalsanctuary:ds-spring-user-framework:4.3.1' // WebAuthn support (Passkey authentication) implementation 'org.springframework.security:spring-security-webauthn' @@ -98,8 +98,6 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-security-test' testImplementation 'org.springframework.security:spring-security-test' testImplementation 'com.h2database:h2:2.4.240' - testImplementation 'com.codeborne:selenide:7.15.0' - testImplementation 'io.github.bonigarcia:webdrivermanager:6.3.3' // OAuth2 Testing dependencies testImplementation 'org.wiremock:wiremock-standalone:3.13.2' @@ -110,7 +108,6 @@ dependencies { test { useJUnitPlatform { - excludeTags 'ui' } testLogging { events "PASSED", "FAILED", "SKIPPED" @@ -119,15 +116,6 @@ test { } } -tasks.register('uiTest', Test) { - useJUnitPlatform { - includeTags 'ui' - } - testClassesDirs = sourceSets.test.output.classesDirs - classpath = sourceSets.test.runtimeClasspath - shouldRunAfter test -} - bootRun { // Use Spring Boot DevTool only when we run Gradle bootRun task classpath = sourceSets.main.runtimeClasspath + configurations.developmentOnly diff --git a/playwright/src/pages/BasePage.ts b/playwright/src/pages/BasePage.ts index e308e9b..6020aaf 100644 --- a/playwright/src/pages/BasePage.ts +++ b/playwright/src/pages/BasePage.ts @@ -108,7 +108,7 @@ export abstract class BasePage { * Wait for navigation to complete. */ async waitForNavigation(): Promise { - await this.page.waitForLoadState('networkidle'); + await this.page.waitForLoadState('domcontentloaded'); } /** diff --git a/playwright/src/pages/EventDetailsPage.ts b/playwright/src/pages/EventDetailsPage.ts index 50d81ed..b311d40 100644 --- a/playwright/src/pages/EventDetailsPage.ts +++ b/playwright/src/pages/EventDetailsPage.ts @@ -88,7 +88,7 @@ export class EventDetailsPage extends BasePage { this.page.waitForEvent('load'), dialog.accept(), ]); - await this.page.waitForLoadState('networkidle'); + await this.page.waitForLoadState('domcontentloaded'); return succeeded; } @@ -108,7 +108,7 @@ export class EventDetailsPage extends BasePage { this.page.waitForEvent('load'), dialog.accept(), ]); - await this.page.waitForLoadState('networkidle'); + await this.page.waitForLoadState('domcontentloaded'); return succeeded; } diff --git a/playwright/tests/access-control/protected-pages.spec.ts b/playwright/tests/access-control/protected-pages.spec.ts index c3ac8fa..89c14d5 100644 --- a/playwright/tests/access-control/protected-pages.spec.ts +++ b/playwright/tests/access-control/protected-pages.spec.ts @@ -8,7 +8,7 @@ test.describe('Access Control', () => { }) => { // Try to access protected page without logging in await protectedPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be redirected to login expect(page.url()).toContain('login'); @@ -28,7 +28,7 @@ test.describe('Access Control', () => { // Access protected page await protectedPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be on protected page expect(page.url()).toContain('protected'); @@ -40,7 +40,7 @@ test.describe('Access Control', () => { }) => { // Try to access user profile without logging in await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be redirected to login expect(page.url()).toContain('login'); @@ -52,7 +52,7 @@ test.describe('Access Control', () => { }) => { // Try to access password change without logging in await updatePasswordPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be redirected to login expect(page.url()).toContain('login'); @@ -64,7 +64,7 @@ test.describe('Access Control', () => { }) => { // Try to access delete account without logging in await deleteAccountPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be redirected to login expect(page.url()).toContain('login'); @@ -76,7 +76,7 @@ test.describe('Access Control', () => { page, }) => { await page.goto('/'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should stay on home page expect(page.url()).not.toContain('login'); @@ -87,7 +87,7 @@ test.describe('Access Control', () => { loginPage, }) => { await loginPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be on login page expect(page.url()).toContain('login'); @@ -98,7 +98,7 @@ test.describe('Access Control', () => { registerPage, }) => { await registerPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be on registration page expect(page.url()).toContain('register'); @@ -109,7 +109,7 @@ test.describe('Access Control', () => { forgotPasswordPage, }) => { await forgotPasswordPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be on forgot password page expect(page.url()).toContain('forgot-password'); @@ -120,7 +120,7 @@ test.describe('Access Control', () => { eventListPage, }) => { await eventListPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be on events page expect(page.url()).toContain('event'); @@ -130,7 +130,7 @@ test.describe('Access Control', () => { page, }) => { await page.goto('/about.html'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be on about page (not redirected) expect(page.url()).toContain('about'); @@ -152,7 +152,7 @@ test.describe('Access Control', () => { // Try to access admin page await adminActionsPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be denied (403 or error page) // With @PreAuthorize, the URL stays the same but shows error page @@ -184,11 +184,11 @@ test.describe('Access Control', () => { // Navigate to multiple protected pages await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); expect(page.url()).toContain('update-user'); await page.goto('/event/my-events.html'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); expect(page.url()).toContain('my-events'); // Should still be logged in @@ -213,11 +213,11 @@ test.describe('Access Control', () => { // Logout await loginPage.logout(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Try to access protected page await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be redirected to login expect(page.url()).toContain('login'); diff --git a/playwright/tests/auth/email-verification.spec.ts b/playwright/tests/auth/email-verification.spec.ts index f5b8aef..b0de360 100644 --- a/playwright/tests/auth/email-verification.spec.ts +++ b/playwright/tests/auth/email-verification.spec.ts @@ -33,7 +33,7 @@ test.describe('Email Verification', () => { expect(verificationUrl).not.toBeNull(); await page.goto(verificationUrl!); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should redirect to registration complete page expect(page.url()).toContain('registration-complete'); @@ -83,7 +83,7 @@ test.describe('Email Verification', () => { }) => { // Navigate to verification URL with invalid token await page.goto('/user/registrationConfirm?token=invalid-token-12345'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should show error or redirect to error page const url = page.url(); @@ -121,7 +121,7 @@ test.describe('Email Verification', () => { // Use a fake expired token (any invalid UUID) await page.goto('/user/registrationConfirm?token=expired-invalid-token-12345'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should show error (same handling as invalid token) const url = page.url(); @@ -168,7 +168,7 @@ test.describe('Email Verification', () => { // Try to use the same token again await page.goto(verificationUrl!); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should either show error or redirect to registration-complete (idempotent behavior) // Both are acceptable - key thing is user stays verified @@ -197,7 +197,7 @@ test.describe('Email Verification', () => { // Navigate to resend verification page await page.goto('/user/request-new-verification-email.html'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Page should load (specific implementation may vary) expect(page.url()).toContain('verification'); diff --git a/playwright/tests/auth/login.spec.ts b/playwright/tests/auth/login.spec.ts index 2415831..a4460d1 100644 --- a/playwright/tests/auth/login.spec.ts +++ b/playwright/tests/auth/login.spec.ts @@ -60,7 +60,7 @@ test.describe('Login', () => { await loginPage.submit(); // Should be redirected to originally requested page or success page - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); }); }); @@ -180,7 +180,7 @@ test.describe('Login', () => { await loginPage.submit(); // Should show error or redirect to verification page - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); const url = page.url(); const hasError = await loginPage.hasError(); diff --git a/playwright/tests/auth/password-reset.spec.ts b/playwright/tests/auth/password-reset.spec.ts index fd0c146..5d3cbcf 100644 --- a/playwright/tests/auth/password-reset.spec.ts +++ b/playwright/tests/auth/password-reset.spec.ts @@ -39,7 +39,7 @@ test.describe('Password Reset', () => { await forgotPasswordPage.requestReset('nonexistent-user-12345@example.com'); // Wait for response - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should either show generic message (for security) or redirect to pending page // Most secure implementations show success even for non-existent emails @@ -81,7 +81,7 @@ test.describe('Password Reset', () => { // Navigate to reset page await page.goto(resetUrl!); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Fill in new password const newPassword = 'NewTest@Pass456!'; @@ -123,7 +123,7 @@ test.describe('Password Reset', () => { await forgotPasswordPage.requestResetAndWait(user.email); const resetUrl = await testApiClient.getPasswordResetUrl(user.email); await page.goto(resetUrl!); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); const newPassword = 'NewTest@Pass789!'; await forgotPasswordChangePage.fillForm(newPassword); @@ -136,7 +136,7 @@ test.describe('Password Reset', () => { await loginPage.goto(); await loginPage.fillCredentials(user.email, originalPassword); await loginPage.submit(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should NOT be logged in (old password should fail) // The login page redirects back to itself on failure @@ -150,7 +150,7 @@ test.describe('Password Reset', () => { }) => { // Navigate to reset page with invalid token await page.goto('/user/changePassword?token=invalid-reset-token-12345'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should show error const url = page.url(); @@ -189,7 +189,7 @@ test.describe('Password Reset', () => { // Get reset token URL const resetUrl = await testApiClient.getPasswordResetUrl(user.email); await page.goto(resetUrl!); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Try to set a weak password await forgotPasswordChangePage.fillForm('weak'); @@ -229,12 +229,12 @@ test.describe('Password Reset', () => { // Get reset token URL const resetUrl = await testApiClient.getPasswordResetUrl(user.email); await page.goto(resetUrl!); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Try to set mismatched passwords await forgotPasswordChangePage.fillForm('NewTest@Pass123!', 'DifferentPass@456!'); await forgotPasswordChangePage.submit(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should show error or stay on page (client-side validation) }); diff --git a/playwright/tests/auth/passwordless-registration.spec.ts b/playwright/tests/auth/passwordless-registration.spec.ts index 536484e..8a2bd08 100644 --- a/playwright/tests/auth/passwordless-registration.spec.ts +++ b/playwright/tests/auth/passwordless-registration.spec.ts @@ -6,7 +6,7 @@ test.describe('Passwordless Registration', () => { registerPage, }) => { await registerPage.goto(); - await registerPage.page.waitForLoadState('networkidle'); + await registerPage.page.waitForLoadState('domcontentloaded'); // The toggle should be visible (WebAuthn is supported in Chromium) const toggle = registerPage.page.locator('#registrationModeToggle'); @@ -27,7 +27,7 @@ test.describe('Passwordless Registration', () => { registerPage, }) => { await registerPage.goto(); - await registerPage.page.waitForLoadState('networkidle'); + await registerPage.page.waitForLoadState('domcontentloaded'); // Password fields should be visible initially const passwordFields = registerPage.page.locator('#passwordFields'); @@ -50,7 +50,7 @@ test.describe('Passwordless Registration', () => { registerPage, }) => { await registerPage.goto(); - await registerPage.page.waitForLoadState('networkidle'); + await registerPage.page.waitForLoadState('domcontentloaded'); // Switch to passwordless await registerPage.page.locator('#modePasswordless').click(); @@ -74,7 +74,7 @@ test.describe('Passwordless Registration', () => { registerPage, }) => { await registerPage.goto(); - await registerPage.page.waitForLoadState('networkidle'); + await registerPage.page.waitForLoadState('domcontentloaded'); const passwordBtn = registerPage.page.locator('#modePassword'); const passwordlessBtn = registerPage.page.locator('#modePasswordless'); @@ -94,7 +94,7 @@ test.describe('Passwordless Registration', () => { registerPage, }) => { await registerPage.goto(); - await registerPage.page.waitForLoadState('networkidle'); + await registerPage.page.waitForLoadState('domcontentloaded'); // Switch to passwordless await registerPage.page.locator('#modePasswordless').click(); @@ -112,7 +112,7 @@ test.describe('Passwordless Registration', () => { registerPage, }) => { await registerPage.goto(); - await registerPage.page.waitForLoadState('networkidle'); + await registerPage.page.waitForLoadState('domcontentloaded'); // Password fields should be required initially await expect(registerPage.passwordInput).toHaveAttribute('required', ''); @@ -138,7 +138,7 @@ test.describe('Passwordless Registration', () => { registerPage, }) => { await registerPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Switch to passwordless mode await page.locator('#modePasswordless').click(); @@ -174,7 +174,7 @@ test.describe('Passwordless Registration', () => { registerPage, }) => { await registerPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Stay in password mode (default) await registerPage.fillForm('Test', 'User', 'test-standard@example.com', 'Test@Pass123!'); diff --git a/playwright/tests/auth/registration.spec.ts b/playwright/tests/auth/registration.spec.ts index 3458eb6..b2c6ed3 100644 --- a/playwright/tests/auth/registration.spec.ts +++ b/playwright/tests/auth/registration.spec.ts @@ -89,16 +89,11 @@ test.describe('Registration', () => { await registerPage.acceptTerms(); await registerPage.submit(); - // Wait for response - await page.waitForLoadState('networkidle'); - - // Should show error or redirect with error parameter - const url = page.url(); - const hasError = await registerPage.hasGlobalError() || - await registerPage.hasExistingAccountError() || - url.includes('error'); - - expect(hasError).toBe(true); + // Registration uses async fetch — wait for the error element to appear + // rather than waiting for page navigation (which doesn't happen) + const globalError = page.locator('#globalError'); + const existingAccountError = page.locator('#existingAccountError'); + await expect(globalError.or(existingAccountError)).toBeVisible({ timeout: 10000 }); }); test('should reject mismatched passwords', async ({ @@ -140,7 +135,7 @@ test.describe('Registration', () => { await registerPage.submit(); // Wait for validation response - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should either show error or stay on registration page const url = page.url(); diff --git a/playwright/tests/e2e/complete-user-journey.spec.ts b/playwright/tests/e2e/complete-user-journey.spec.ts index 7abb7d3..12ca9c0 100644 --- a/playwright/tests/e2e/complete-user-journey.spec.ts +++ b/playwright/tests/e2e/complete-user-journey.spec.ts @@ -71,7 +71,7 @@ test.describe('Complete User Journey', () => { // ========================================== await test.step('Update profile', async () => { await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Verify current values expect(await updateUserPage.getFirstName()).toBe(user.firstName); @@ -94,7 +94,7 @@ test.describe('Complete User Journey', () => { // ========================================== await test.step('Change password', async () => { await updatePasswordPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Change password await updatePasswordPage.changePasswordAndWait(user.password, newPassword); @@ -114,12 +114,12 @@ test.describe('Complete User Journey', () => { let registeredForEvent = false; await test.step('Register for event', async () => { await eventListPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); const eventCount = await eventListPage.getEventCount(); if (eventCount > 0) { await eventListPage.clickEventByIndex(0); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); if (await eventDetailsPage.canRegister()) { registeredForEvent = await eventDetailsPage.register(); @@ -127,7 +127,7 @@ test.describe('Complete User Journey', () => { // Server-side rendered page may show stale state under concurrent load; // if the API confirmed success but the page hasn't caught up, reload once. if (!await eventDetailsPage.canUnregister()) { - await page.reload({ waitUntil: 'networkidle' }); + await page.reload({ waitUntil: 'domcontentloaded' }); } expect(await eventDetailsPage.canUnregister()).toBe(true); } @@ -152,7 +152,7 @@ test.describe('Complete User Journey', () => { // ========================================== await test.step('Delete account', async () => { await deleteAccountPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Delete account await deleteAccountPage.deleteAccountAndWait(); @@ -168,7 +168,7 @@ test.describe('Complete User Journey', () => { await loginPage.goto(); await loginPage.fillCredentials(user.email, newPassword); await loginPage.submit(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should show error or be redirected expect(await loginPage.hasError() || !await loginPage.isLoggedIn()).toBe(true); @@ -191,7 +191,7 @@ test.describe('Complete User Journey', () => { ); await registerPage.acceptTerms(); await registerPage.submit(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should stay on registration page or show error expect(page.url()).toContain('register'); @@ -220,7 +220,7 @@ test.describe('Complete User Journey', () => { await test.step('Access protected page without auth redirects to login', async () => { await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should redirect to login expect(page.url()).toContain('login'); @@ -229,14 +229,14 @@ test.describe('Complete User Journey', () => { await test.step('Login and verify access to protected page', async () => { await loginPage.fillCredentials(user.email, user.password); await loginPage.submit(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be logged in expect(await loginPage.isLoggedIn()).toBe(true); // Access protected page await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be on protected page expect(page.url()).toContain('update-user'); @@ -276,7 +276,7 @@ test.describe('Complete User Journey', () => { expect(resetUrl).not.toBeNull(); await page.goto(resetUrl!); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); await forgotPasswordChangePage.fillForm(newPassword); await forgotPasswordChangePage.submit(); @@ -295,7 +295,7 @@ test.describe('Complete User Journey', () => { await loginPage.goto(); await loginPage.fillCredentials(user.email, originalPassword); await loginPage.submit(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); expect(await loginPage.hasError()).toBe(true); }); diff --git a/playwright/tests/events/browse-events.spec.ts b/playwright/tests/events/browse-events.spec.ts index 4b25e0a..209069d 100644 --- a/playwright/tests/events/browse-events.spec.ts +++ b/playwright/tests/events/browse-events.spec.ts @@ -8,7 +8,7 @@ test.describe('Browse Events', () => { }) => { // Navigate to events page without logging in await eventListPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should display the events page expect(page.url()).toContain('event'); @@ -26,7 +26,7 @@ test.describe('Browse Events', () => { }) => { // Navigate to events page await eventListPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // If there are events, verify they have expected content const eventCount = await eventListPage.getEventCount(); @@ -49,13 +49,13 @@ test.describe('Browse Events', () => { }) => { // Navigate to events page await eventListPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // If there are events, click on one const eventCount = await eventListPage.getEventCount(); if (eventCount > 0) { await eventListPage.clickEventByIndex(0); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be on event details page expect(page.url()).toContain('details'); @@ -71,13 +71,13 @@ test.describe('Browse Events', () => { }) => { // Navigate to events page await eventListPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // If there are events, go to details const eventCount = await eventListPage.getEventCount(); if (eventCount > 0) { await eventListPage.clickEventByIndex(0); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should show login prompt (not register/unregister buttons) const hasLoginPrompt = await eventDetailsPage.hasLoginPrompt(); diff --git a/playwright/tests/events/event-registration.spec.ts b/playwright/tests/events/event-registration.spec.ts index 3b25876..861fd61 100644 --- a/playwright/tests/events/event-registration.spec.ts +++ b/playwright/tests/events/event-registration.spec.ts @@ -17,18 +17,18 @@ test.describe('Event Registration', () => { // Navigate to events page await eventListPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // If there are events, register for one const eventCount = await eventListPage.getEventCount(); if (eventCount > 0) { await eventListPage.clickEventByIndex(0); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Check if we can register if (await eventDetailsPage.canRegister()) { await eventDetailsPage.register(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Wait for page to update and show unregister button await page.locator('button:has-text("Unregister")').waitFor({ state: 'visible', timeout: 5000 }); @@ -54,13 +54,13 @@ test.describe('Event Registration', () => { // Navigate to events page await eventListPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // If there are events, check details page const eventCount = await eventListPage.getEventCount(); if (eventCount > 0) { await eventListPage.clickEventByIndex(0); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Either register or unregister button should be visible const canRegister = await eventDetailsPage.canRegister(); @@ -87,24 +87,24 @@ test.describe('Event Registration', () => { // Navigate to events page await eventListPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // If there are events const eventCount = await eventListPage.getEventCount(); if (eventCount > 0) { await eventListPage.clickEventByIndex(0); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // If not registered, register first if (await eventDetailsPage.canRegister()) { await eventDetailsPage.register(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); } // Now unregister if (await eventDetailsPage.canUnregister()) { await eventDetailsPage.unregister(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // After unregistering, should show register button expect(await eventDetailsPage.canRegister()).toBe(true); @@ -127,7 +127,7 @@ test.describe('Event Registration', () => { // Navigate to my events page await page.goto('/event/my-events.html'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be on my events page (not redirected to login) expect(page.url()).toContain('my-events'); @@ -138,7 +138,7 @@ test.describe('Event Registration', () => { }) => { // Access my events page without logging in await page.goto('/event/my-events.html'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Page should load (it's public but shows personalized content when logged in) expect(page.url()).toContain('my-events'); @@ -156,17 +156,17 @@ test.describe('Event Registration', () => { }) => { // Navigate to events page await eventListPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // If there are events, go to details and back const eventCount = await eventListPage.getEventCount(); if (eventCount > 0) { await eventListPage.clickEventByIndex(0); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Go back to events list await eventDetailsPage.goBackToEvents(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); expect(page.url()).toContain('event'); } diff --git a/playwright/tests/profile/auth-methods.spec.ts b/playwright/tests/profile/auth-methods.spec.ts index 214dc42..10a01e6 100644 --- a/playwright/tests/profile/auth-methods.spec.ts +++ b/playwright/tests/profile/auth-methods.spec.ts @@ -13,7 +13,7 @@ test.describe('Authentication Methods', () => { await createAndLoginUser(page, testApiClient, user); await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Auth methods section should become visible after JS loads const authSection = page.locator('#auth-methods-section'); @@ -32,7 +32,7 @@ test.describe('Authentication Methods', () => { await createAndLoginUser(page, testApiClient, user); await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Wait for auth methods to load const badgesContainer = page.locator('#auth-method-badges'); @@ -54,7 +54,7 @@ test.describe('Authentication Methods', () => { await createAndLoginUser(page, testApiClient, user); await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Wait for auth methods section to load const authSection = page.locator('#auth-methods-section'); @@ -76,7 +76,7 @@ test.describe('Authentication Methods', () => { await createAndLoginUser(page, testApiClient, user); await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Wait for auth methods section to load const authSection = page.locator('#auth-methods-section'); @@ -100,7 +100,7 @@ test.describe('Authentication Methods', () => { await createAndLoginUser(page, testApiClient, user); await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Passkey section should be visible const passkeySection = page.locator('#passkey-section'); @@ -126,7 +126,7 @@ test.describe('Authentication Methods', () => { await createAndLoginUser(page, testApiClient, user); await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Wait for passkeys to load const passkeysList = page.locator('#passkeys-list'); @@ -150,7 +150,7 @@ test.describe('Authentication Methods', () => { await createAndLoginUser(page, testApiClient, user); await updatePasswordPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Current password section should be visible const currentPasswordSection = page.locator('#currentPasswordSection'); @@ -179,7 +179,7 @@ test.describe('Authentication Methods', () => { await createAndLoginUser(page, testApiClient, user); await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Wait for auth methods to load so link text updates const authSection = page.locator('#auth-methods-section'); diff --git a/playwright/tests/profile/change-password.spec.ts b/playwright/tests/profile/change-password.spec.ts index ac0f438..f95a549 100644 --- a/playwright/tests/profile/change-password.spec.ts +++ b/playwright/tests/profile/change-password.spec.ts @@ -18,7 +18,7 @@ test.describe('Change Password', () => { // Navigate to change password page await updatePasswordPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Change password const newPassword = 'NewTest@Pass456!'; @@ -53,7 +53,7 @@ test.describe('Change Password', () => { // Navigate to change password page await updatePasswordPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Change password const newPassword = 'NewTest@Pass789!'; @@ -89,7 +89,7 @@ test.describe('Change Password', () => { // Navigate to change password page await updatePasswordPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Try to change password with wrong current password // Listen for the server response to verify error handling @@ -133,15 +133,14 @@ test.describe('Change Password', () => { // Navigate to change password page await updatePasswordPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); - // Try to change to a weak password + // Try to change to a weak password and wait for the server response await updatePasswordPage.changePassword(user.password, 'weak'); - await page.waitForLoadState('networkidle'); + await updatePasswordPage.waitForMessage(10000); - // Should show error or validation message - const url = page.url(); - expect(url).toContain('update-password'); + // Should show an error message (weak password rejected) + expect(await updatePasswordPage.isErrorMessage()).toBe(true); }); test('should reject mismatched new passwords', async ({ @@ -158,7 +157,7 @@ test.describe('Change Password', () => { // Navigate to change password page await updatePasswordPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Try to change with mismatched passwords await updatePasswordPage.fillForm( @@ -167,9 +166,9 @@ test.describe('Change Password', () => { 'DifferentPass@456!' ); await updatePasswordPage.submit(); - await page.waitForLoadState('networkidle'); - // Should show error or validation message (client-side validation) + // Client-side validation catches mismatched passwords — error div should appear + await expect(updatePasswordPage.confirmPasswordError).toBeVisible({ timeout: 5000 }); }); test('should reject password same as current', async ({ @@ -186,11 +185,11 @@ test.describe('Change Password', () => { // Navigate to change password page await updatePasswordPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Try to change to the same password await updatePasswordPage.changePassword(user.password, user.password); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // May or may not be rejected depending on policy }); @@ -213,7 +212,7 @@ test.describe('Change Password', () => { // Navigate to change password page await updatePasswordPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Change password const newPassword = 'NewTest@Pass111!'; @@ -221,10 +220,10 @@ test.describe('Change Password', () => { // Now try to change back to original password await updatePasswordPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); await updatePasswordPage.changePassword(newPassword, originalPassword); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should reject due to password history (if enabled) // Behavior depends on configuration @@ -238,7 +237,7 @@ test.describe('Change Password', () => { }) => { // Try to access password change page without logging in await updatePasswordPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be redirected to login expect(page.url()).toContain('login'); diff --git a/playwright/tests/profile/delete-account.spec.ts b/playwright/tests/profile/delete-account.spec.ts index 2da73c5..13c2fdf 100644 --- a/playwright/tests/profile/delete-account.spec.ts +++ b/playwright/tests/profile/delete-account.spec.ts @@ -17,7 +17,7 @@ test.describe('Delete Account', () => { // Navigate to delete account page await deleteAccountPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Delete account await deleteAccountPage.deleteAccountAndWait(); @@ -46,7 +46,7 @@ test.describe('Delete Account', () => { // Navigate to delete account page await deleteAccountPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Delete account and wait for success message await deleteAccountPage.deleteAccountAndWait(); @@ -54,7 +54,7 @@ test.describe('Delete Account', () => { // The page shows success message but session is invalidated server-side. // Navigate to a protected page to verify we're logged out. await page.goto('/user/update-user.html'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be redirected to login (session was invalidated) expect(page.url()).toContain('login'); @@ -77,7 +77,7 @@ test.describe('Delete Account', () => { // Navigate to delete account page await deleteAccountPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Delete account await deleteAccountPage.deleteAccountAndWait(); @@ -86,7 +86,7 @@ test.describe('Delete Account', () => { await loginPage.goto(); await loginPage.fillCredentials(user.email, password); await loginPage.submit(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should show error const hasError = await loginPage.hasError(); @@ -111,7 +111,7 @@ test.describe('Delete Account', () => { // Navigate to delete account page await deleteAccountPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Click delete button await deleteAccountPage.clickDelete(); @@ -135,7 +135,7 @@ test.describe('Delete Account', () => { // Navigate to delete account page await deleteAccountPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Open modal and cancel await deleteAccountPage.clickDelete(); @@ -165,7 +165,7 @@ test.describe('Delete Account', () => { // Navigate to delete account page await deleteAccountPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Open modal await deleteAccountPage.clickDelete(); @@ -190,7 +190,7 @@ test.describe('Delete Account', () => { }) => { // Try to access delete account page without logging in await deleteAccountPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be redirected to login expect(page.url()).toContain('login'); diff --git a/playwright/tests/profile/update-profile.spec.ts b/playwright/tests/profile/update-profile.spec.ts index 0a66084..835ccc1 100644 --- a/playwright/tests/profile/update-profile.spec.ts +++ b/playwright/tests/profile/update-profile.spec.ts @@ -16,7 +16,7 @@ test.describe('Update Profile', () => { // Navigate to update user page await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Update first name const newFirstName = 'UpdatedFirst'; @@ -44,7 +44,7 @@ test.describe('Update Profile', () => { // Navigate to update user page await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Update last name const newLastName = 'UpdatedLast'; @@ -72,7 +72,7 @@ test.describe('Update Profile', () => { // Navigate to update user page await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Update both names const newFirstName = 'NewFirst'; @@ -108,7 +108,7 @@ test.describe('Update Profile', () => { // Navigate to update user page await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Verify current values are shown const currentFirst = await updateUserPage.getFirstName(); @@ -134,7 +134,7 @@ test.describe('Update Profile', () => { // Navigate to update user page await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Navigate to change password await updateUserPage.goToChangePassword(); @@ -156,7 +156,7 @@ test.describe('Update Profile', () => { // Navigate to update user page await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Navigate to delete account await updateUserPage.goToDeleteAccount(); @@ -165,6 +165,60 @@ test.describe('Update Profile', () => { }); }); + test.describe('Validation', () => { + test('should handle empty field submission gracefully', async ({ + page, + updateUserPage, + testApiClient, + cleanupEmails, + }) => { + const user = generateTestUser('update-empty'); + cleanupEmails.push(user.email); + + await createAndLoginUser(page, testApiClient, user); + + await updateUserPage.goto(); + await page.waitForLoadState('domcontentloaded'); + + // Clear both fields and attempt to submit + await updateUserPage.firstNameInput.fill(''); + await updateUserPage.lastNameInput.fill(''); + await updateUserPage.submit(); + + // HTML5 required validation should block submission — inputs should be invalid + const firstNameIsValid = await updateUserPage.firstNameInput.evaluate( + (el: HTMLInputElement) => el.checkValidity() + ); + const lastNameIsValid = await updateUserPage.lastNameInput.evaluate( + (el: HTMLInputElement) => el.checkValidity() + ); + expect(firstNameIsValid).toBe(false); + expect(lastNameIsValid).toBe(false); + }); + + test('should handle excessively long names gracefully', async ({ + page, + updateUserPage, + testApiClient, + cleanupEmails, + }) => { + const user = generateTestUser('update-long'); + cleanupEmails.push(user.email); + + await createAndLoginUser(page, testApiClient, user); + + await updateUserPage.goto(); + await page.waitForLoadState('domcontentloaded'); + + // Submit with very long names (300+ chars) and wait for server response + const longName = 'A'.repeat(300); + await updateUserPage.updateProfileAndWait(longName, longName); + + // App should handle gracefully — success or error message, not crash + expect(await updateUserPage.globalMessage.isVisible()).toBe(true); + }); + }); + test.describe('Access Control', () => { test('should require authentication to access update page', async ({ page, @@ -172,7 +226,7 @@ test.describe('Update Profile', () => { }) => { // Try to access update page without logging in await updateUserPage.goto(); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Should be redirected to login expect(page.url()).toContain('login'); diff --git a/src/main/java/com/digitalsanctuary/spring/demo/test/api/TestDataController.java b/src/main/java/com/digitalsanctuary/spring/demo/test/api/TestDataController.java index 7e5c5da..1aebd08 100644 --- a/src/main/java/com/digitalsanctuary/spring/demo/test/api/TestDataController.java +++ b/src/main/java/com/digitalsanctuary/spring/demo/test/api/TestDataController.java @@ -3,6 +3,7 @@ import java.time.LocalDateTime; import java.util.Collections; import java.time.Instant; +import java.util.Date; import java.util.HashMap; import java.util.Map; import org.springframework.context.annotation.Profile; @@ -193,7 +194,7 @@ public ResponseEntity> createTestUser(@RequestBody CreateUse user.setEnabled(request.enabled() != null ? request.enabled() : true); user.setLocked(false); user.setFailedLoginAttempts(0); - user.setRegistrationDate(Instant.now()); + user.setRegistrationDate(Date.from(Instant.now())); // Assign default role Role userRole = roleRepository.findByName("ROLE_USER"); diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/BaseUiTest.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/BaseUiTest.java deleted file mode 100644 index c309bf1..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/BaseUiTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui; - -import com.codeborne.selenide.Configuration; -import com.codeborne.selenide.Selenide; -import io.github.bonigarcia.wdm.WebDriverManager; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; - -public abstract class BaseUiTest { - private Driver driver; - - public enum Driver { - CHROME("chrome"), OPERA("opera"), FIREFOX("firefox"), EDGE("edge"); - - private final String browser; - - Driver(String browser) { - this.browser = browser; - } - } - - public void setUp() { - switch (this.driver) { - case CHROME -> WebDriverManager.chromedriver().setup(); - case OPERA -> WebDriverManager.operadriver().setup(); - case FIREFOX -> WebDriverManager.firefoxdriver().setup(); - case EDGE -> WebDriverManager.edgedriver().setup(); - } - Configuration.browser = driver.browser; - Configuration.browserSize = "2560x1440"; - Configuration.webdriverLogsEnabled = true; - Configuration.headless = false; - } - - @BeforeEach - public void init() { - setUp(); - } - - @AfterEach - public void tearDown() { - Selenide.closeWebDriver(); - } - - void setDriver(Driver driver) { - this.driver = driver; - } - -} diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/CompleteUserJourneyE2ETest.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/CompleteUserJourneyE2ETest.java deleted file mode 100644 index b97aeff..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/CompleteUserJourneyE2ETest.java +++ /dev/null @@ -1,431 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui; - -import com.codeborne.selenide.Configuration; -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.Screenshots; -import com.digitalsanctuary.spring.demo.user.ui.page.*; -import com.digitalsanctuary.spring.demo.user.ui.util.DatabaseStateValidator; -import com.digitalsanctuary.spring.demo.user.ui.util.EmailVerificationSimulator; -import com.digitalsanctuary.spring.user.dto.UserDto; -import com.digitalsanctuary.spring.user.jdbc.Jdbc; -import org.junit.jupiter.api.*; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; - -import java.io.File; -import java.io.IOException; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Complete User Journey E2E Test as specified in Task 4.1 of TEST-IMPROVEMENT-PLAN.md - * - * Tests the complete user lifecycle from registration to deletion including: - * - User Registration with form validation - * - Email Verification simulation - * - Profile Management (update user details) - * - Password Change functionality - * - Password Reset flow - * - Account Deletion with confirmation - * - * Features: - * - Multi-browser testing (Chrome, Firefox, Edge) - * - Database state validation at each step - * - Screenshot capture on failures - * - Error scenario testing - * - Async operation handling - * - CSRF protection verification - */ -@Tag("ui") -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@ActiveProfiles("test") -@DisplayName("Complete User Journey E2E Tests") -public class CompleteUserJourneyE2ETest extends BaseUiTest { - - private static final String BASE_URI = "http://localhost:8080"; - private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"); - - @Value("${test.browser}") - private String defaultBrowser; - - // Test data - private UserDto testUser; - private String testEmail; - private String originalPassword; - private String newPassword; - - @BeforeAll - public void setupBrowser() { - // Configure screenshot settings - Configuration.reportsFolder = "build/reports/selenide-screenshots"; - Configuration.screenshots = true; - Configuration.savePageSource = true; - } - - @BeforeEach - public void setupTestData() { - // Generate unique test data for each test - String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT); - testEmail = "journey.test." + timestamp + "@example.com"; - originalPassword = "OriginalPass123!"; - newPassword = "NewPassword456!"; - - testUser = new UserDto(); - testUser.setFirstName("Journey"); - testUser.setLastName("Tester" + timestamp); - testUser.setEmail(testEmail); - testUser.setPassword(originalPassword); - testUser.setMatchingPassword(originalPassword); - } - - @AfterEach - public void cleanup() { - try { - // Clean up test user data - Jdbc.deleteTestUser(testUser); - } catch (Exception e) { - // Ignore cleanup errors for deleted accounts - } - } - - @Nested - @DisplayName("Complete User Journey Tests") - class CompleteUserJourneyTests { - - @ParameterizedTest - @EnumSource(value = Driver.class, names = {"CHROME", "FIREFOX", "EDGE"}) - @DisplayName("Complete Happy Path User Journey") - void completeHappyPathUserJourney(Driver browser) throws IOException { - setDriver(browser); - setUp(); - - try { - // Step 1: User Registration - performUserRegistration(); - - // Step 2: Email Verification - performEmailVerification(); - - // Step 3: Profile Management - performProfileUpdate(); - - // Step 4: Password Change - performPasswordChange(); - - // Step 5: Password Reset Flow (testing the flow) - performPasswordResetFlow(); - - // Step 6: Account Deletion - performAccountDeletion(); - - } catch (Exception e) { - captureScreenshotOnFailure(browser.name() + "_CompleteJourney"); - throw e; - } - } - - @Test - @DisplayName("User Journey with Error Scenarios") - void userJourneyWithErrorScenarios() throws IOException { - setDriver(Driver.CHROME); - setUp(); - - try { - // Register user first - performUserRegistration(); - performEmailVerification(); - - // Test error scenarios - testProfileUpdateErrors(); - testPasswordChangeErrors(); - testAccountDeletionErrors(); - - } catch (Exception e) { - captureScreenshotOnFailure("ErrorScenarios"); - throw e; - } - } - - @Test - @DisplayName("Email Verification Edge Cases") - void emailVerificationEdgeCases() throws IOException { - setDriver(Driver.CHROME); - setUp(); - - try { - // Register user - performUserRegistration(); - - // Test invalid verification token - testInvalidEmailVerification(); - - // Test valid verification - performEmailVerification(); - - } catch (Exception e) { - captureScreenshotOnFailure("EmailVerificationEdgeCases"); - throw e; - } - } - - @Test - @DisplayName("Concurrent User Operations") - void concurrentUserOperations() throws IOException { - setDriver(Driver.CHROME); - setUp(); - - try { - // Test concurrent registration attempts - testConcurrentRegistration(); - - } catch (Exception e) { - captureScreenshotOnFailure("ConcurrentOperations"); - throw e; - } - } - } - - // Helper methods for test steps - - private void performUserRegistration() { - RegisterPage registerPage = new RegisterPage(BASE_URI + "/user/register.html"); - - // Fill and submit registration form - SuccessRegisterPage successPage = registerPage.signUp( - testUser.getFirstName(), - testUser.getLastName(), - testUser.getEmail(), - testUser.getPassword(), - testUser.getMatchingPassword() - ); - - // Verify success message - String successMessage = successPage.message(); - assertTrue(successMessage.contains("Thank you for registering"), - "Registration success message not displayed"); - - // Verify database state - DatabaseStateValidator.validateUserRegistered(testEmail, - testUser.getFirstName(), testUser.getLastName()); - - // User should not be enabled yet (pending email verification) - assertFalse(DatabaseStateValidator.isUserEnabled(testEmail), - "User should not be enabled before email verification"); - } - - private void performEmailVerification() { - // Simulate email verification - assertTrue(EmailVerificationSimulator.hasVerificationToken(testEmail), - "Verification token should exist"); - - String verificationToken = EmailVerificationSimulator.getVerificationToken(testEmail); - assertNotNull(verificationToken, "Verification token should not be null"); - - // Simulate clicking verification link - String verificationUrl = EmailVerificationSimulator.generateVerificationUrl(testEmail); - Selenide.open(verificationUrl); - - // Verify database state after verification - DatabaseStateValidator.validateEmailVerified(testEmail); - } - - private void performProfileUpdate() { - // Navigate to update profile page - UpdateUserPage updatePage = new UpdateUserPage(BASE_URI + "/user/update-user.html"); - updatePage.waitForPageLoad(); - - // Verify current profile data - assertEquals(testUser.getFirstName(), updatePage.getFirstName()); - assertEquals(testUser.getLastName(), updatePage.getLastName()); - - // Update profile - String newFirstName = "Updated" + testUser.getFirstName(); - String newLastName = "Updated" + testUser.getLastName(); - - updatePage.updateProfile(newFirstName, newLastName); - - // Verify success message - assertTrue(updatePage.isUpdateSuccessful(), - "Profile update should be successful"); - - // Verify database state - DatabaseStateValidator.validateProfileUpdated(testEmail, newFirstName, newLastName); - - // Update test data for subsequent steps - testUser.setFirstName(newFirstName); - testUser.setLastName(newLastName); - } - - private void performPasswordChange() { - UpdateUserPage updateUserPage = new UpdateUserPage(BASE_URI + "/user/update-user.html"); - UpdatePasswordPage passwordPage = updateUserPage.goToChangePassword(); - passwordPage.waitForPageLoad(); - - // Change password - passwordPage.updatePassword(originalPassword, newPassword); - - // Verify success - assertTrue(passwordPage.isPasswordUpdateSuccessful(), - "Password change should be successful"); - - // Test login with new password - LoginPage loginPage = new LoginPage(BASE_URI + "/user/login.html"); - LoginSuccessPage loginSuccess = loginPage.signIn(testEmail, newPassword); - - String welcomeMessage = loginSuccess.welcomeMessage(); - assertTrue(welcomeMessage.contains(testUser.getFirstName()), - "Login with new password should be successful"); - } - - private void performPasswordResetFlow() { - // Logout first - Selenide.open(BASE_URI + "/user/logout"); - - // Request password reset - ForgotPasswordPage forgotPasswordPage = new ForgotPasswordPage(BASE_URI + "/user/forgot-password.html"); - SuccessResetPasswordPage resetPage = forgotPasswordPage.fillEmail(testEmail).clickSubmitBtn(); - - String resetMessage = resetPage.message(); - assertTrue(resetMessage.contains("password reset email"), - "Password reset request should be successful"); - - // Verify password reset token was created - assertTrue(DatabaseStateValidator.hasPasswordResetToken(testEmail), - "Password reset token should be created"); - } - - private void performAccountDeletion() { - // Login first - LoginPage loginPage = new LoginPage(BASE_URI + "/user/login.html"); - loginPage.signIn(testEmail, newPassword); - - // Navigate to delete account page - DeleteAccountPage deletePage = new DeleteAccountPage(BASE_URI + "/user/delete-account.html"); - deletePage.waitForPageLoad(); - - // Start deletion process - deletePage.startDeletion(); - assertTrue(deletePage.isConfirmationModalVisible(), - "Confirmation modal should be visible"); - - // Complete deletion - deletePage.confirmDeletion(); - - // Verify success - assertTrue(deletePage.isDeletionSuccessful(), - "Account deletion should be successful"); - - // Verify database state - DatabaseStateValidator.validateAccountDeleted(testEmail); - - // Verify cannot login after deletion - LoginPage loginAfterDeletion = new LoginPage(BASE_URI + "/user/login.html"); - loginAfterDeletion.signIn(testEmail, newPassword); - - // Should remain on login page or show error - String currentUrl = Selenide.webdriver().driver().url(); - assertTrue(currentUrl.contains("login"), - "Should not be able to login after account deletion"); - } - - // Error scenario test methods - - private void testProfileUpdateErrors() { - UpdateUserPage updatePage = new UpdateUserPage(BASE_URI + "/user/update-user.html"); - - // Test with empty fields - updatePage.updateProfile("", ""); - // Should show validation errors or remain on same page - - // Test with very long names - String veryLongName = "a".repeat(300); - updatePage.updateProfile(veryLongName, veryLongName); - // Should handle gracefully - } - - private void testPasswordChangeErrors() { - UpdateUserPage updateUserPage = new UpdateUserPage(BASE_URI + "/user/update-user.html"); - UpdatePasswordPage passwordPage = updateUserPage.goToChangePassword(); - - // Test with wrong current password - passwordPage.updatePassword("WrongPassword", "NewPassword123!"); - assertFalse(passwordPage.isPasswordUpdateSuccessful(), - "Password change should fail with wrong current password"); - - // Test with mismatched new passwords - passwordPage.clearAllFields(); - passwordPage.updatePassword(newPassword, "NewPassword123!", "DifferentPassword"); - assertTrue(passwordPage.hasPasswordMismatchError(), - "Should show password mismatch error"); - } - - private void testAccountDeletionErrors() { - DeleteAccountPage deletePage = new DeleteAccountPage(BASE_URI + "/user/delete-account.html"); - - // Start deletion - deletePage.startDeletion(); - - // Test with wrong confirmation text - deletePage.confirmDeletion("WRONG"); - assertTrue(deletePage.hasError(), - "Should show error for wrong confirmation text"); - - // Cancel and try again - deletePage.cancelDeletion(); - assertFalse(deletePage.isConfirmationModalVisible(), - "Modal should be hidden after cancel"); - } - - private void testInvalidEmailVerification() { - String invalidUrl = EmailVerificationSimulator.generateInvalidVerificationUrl(); - Selenide.open(invalidUrl); - - // Should show error or redirect to appropriate page - String currentUrl = Selenide.webdriver().driver().url(); - // Verification should fail gracefully - } - - private void testConcurrentRegistration() { - // This test simulates attempting to register the same email twice - // First registration should succeed, second should fail - - RegisterPage registerPage1 = new RegisterPage(BASE_URI + "/user/register.html"); - registerPage1.signUp( - testUser.getFirstName(), - testUser.getLastName(), - testEmail, - testUser.getPassword(), - testUser.getMatchingPassword() - ); - - // Attempt second registration with same email - RegisterPage registerPage2 = new RegisterPage(BASE_URI + "/user/register.html"); - registerPage2.signUp( - "Different", - "User", - testEmail, // Same email - "DifferentPassword123!", - "DifferentPassword123!" - ); - - // Should show error message - String errorMessage = registerPage2.accountExistErrorMessage(); - assertTrue(errorMessage.contains("already exists"), - "Should show account exists error for duplicate email"); - } - - private void captureScreenshotOnFailure(String testName) throws IOException { - String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT); - String filename = testName + "_" + timestamp; - File screenshot = Screenshots.takeScreenShotAsFile(); - if (screenshot != null) { - System.out.println("Screenshot captured: " + screenshot.getAbsolutePath()); - } - } -} \ No newline at end of file diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/SpringUserFrameworkUiTest.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/SpringUserFrameworkUiTest.java deleted file mode 100644 index 8fdb63f..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/SpringUserFrameworkUiTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui; - - -import static com.digitalsanctuary.spring.demo.user.ui.data.UiTestData.ACCOUNT_EXIST_ERROR_MESSAGE; -import static com.digitalsanctuary.spring.demo.user.ui.data.UiTestData.SUCCESS_RESET_PASSWORD_MESSAGE; -import static com.digitalsanctuary.spring.demo.user.ui.data.UiTestData.SUCCESS_SING_UP_MESSAGE; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import com.digitalsanctuary.spring.demo.user.ui.data.UiTestData; -import com.digitalsanctuary.spring.demo.user.ui.page.ForgotPasswordPage; -import com.digitalsanctuary.spring.demo.user.ui.page.LoginPage; -import com.digitalsanctuary.spring.demo.user.ui.page.LoginSuccessPage; -import com.digitalsanctuary.spring.demo.user.ui.page.RegisterPage; -import com.digitalsanctuary.spring.demo.user.ui.page.SuccessRegisterPage; -import com.digitalsanctuary.spring.demo.user.ui.page.SuccessResetPasswordPage; -import com.digitalsanctuary.spring.user.dto.UserDto; -import com.digitalsanctuary.spring.user.jdbc.Jdbc; - -@Tag("ui") -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@ActiveProfiles("test") -public class SpringUserFrameworkUiTest extends BaseUiTest { - - private static final String URI = "http://localhost:8080/"; - - private static final UserDto testUser = UiTestData.getUserDto(); - - @Value("${test.browser}") - private String browser; - - @BeforeAll - public void setBrowser() { - switch (browser) { - case "chrome" -> super.setDriver(Driver.CHROME); - case "opera" -> super.setDriver(Driver.OPERA); - case "firefox" -> super.setDriver(Driver.FIREFOX); - case "edge" -> super.setDriver(Driver.EDGE); - } - } - - { - super.setDriver(Driver.CHROME); - } - - @AfterEach - public void deleteTestUser() { - Jdbc.deleteTestUser(testUser); - } - - @Test - public void successSignUp() { - SuccessRegisterPage registerPage = new RegisterPage(URI + "user/register.html").signUp(testUser.getFirstName(), testUser.getLastName(), - testUser.getEmail(), testUser.getPassword(), testUser.getMatchingPassword()); - String actualMessage = registerPage.message(); - Assertions.assertEquals(SUCCESS_SING_UP_MESSAGE, actualMessage); - } - - @Test - public void userAlreadyExistSignUp() { - Jdbc.saveTestUser(testUser); - RegisterPage registerPage = new RegisterPage(URI + "user/register.html"); - registerPage.signUp(testUser.getFirstName(), testUser.getLastName(), testUser.getEmail(), testUser.getPassword(), - testUser.getMatchingPassword()); - String actualMessage = registerPage.accountExistErrorMessage(); - Assertions.assertEquals(ACCOUNT_EXIST_ERROR_MESSAGE, actualMessage); - } - - /** - * checks that welcome message in success login page contains username - */ - @Test - public void successSignIn() { - Jdbc.saveTestUser(testUser); - LoginPage loginPage = new LoginPage(URI + "user/login.html"); - LoginSuccessPage loginSuccessPage = loginPage.signIn(testUser.getEmail(), testUser.getPassword()); - String welcomeMessage = loginSuccessPage.welcomeMessage(); - String firstName = testUser.getFirstName(); - Assertions.assertTrue(welcomeMessage.contains(firstName)); - } - - @Test - public void successResetPassword() { - Jdbc.saveTestUser(testUser); - ForgotPasswordPage forgotPasswordPage = new ForgotPasswordPage(URI + "user/forgot-password.html"); - SuccessResetPasswordPage successResetPasswordPage = forgotPasswordPage.fillEmail(testUser.getEmail()).clickSubmitBtn(); - String actualMessage = successResetPasswordPage.message(); - Assertions.assertEquals(SUCCESS_RESET_PASSWORD_MESSAGE, actualMessage); - - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/data/UiTestData.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/data/UiTestData.java deleted file mode 100644 index 811bc7f..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/data/UiTestData.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui.data; - -import com.digitalsanctuary.spring.user.dto.UserDto; - -public class UiTestData { - - public static final String ACCOUNT_EXIST_ERROR_MESSAGE = - "An account for that username/email already exists. " + "Please enter a different email."; - - public static final String SUCCESS_SING_UP_MESSAGE = "Thank you for registering!"; - - public static final String SUCCESS_RESET_PASSWORD_MESSAGE = "You should receive a password reset email shortly"; - - public static final String TEST_USER_ENCODED_PASSWORD = "$2y$10$XIRn/npMMCGt21gpU6QAbeOAUSxj/C7A793YZe.a6AEvL0LhQwkqW"; - - public static UserDto getUserDto() { - UserDto userDto = new UserDto(); - userDto.setFirstName("testUiUser"); - userDto.setLastName("userUiTest"); - userDto.setEmail("testUiUser@bk.com"); - userDto.setPassword("testUiUserPassword"); - userDto.setMatchingPassword(userDto.getPassword()); - return userDto; - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/DeleteAccountPage.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/DeleteAccountPage.java deleted file mode 100644 index 597fb01..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/DeleteAccountPage.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui.page; - -import static com.codeborne.selenide.Condition.appear; -import static com.codeborne.selenide.Condition.cssClass; -import static com.codeborne.selenide.Condition.disappear; -import static com.codeborne.selenide.Condition.visible; -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; - -/** - * Page object for delete account page - */ -public class DeleteAccountPage { - - private final SelenideElement DELETE_ACCOUNT_BUTTON = Selenide.$x("//button[@type='submit']"); - private final SelenideElement DELETE_CONFIRMATION_MODAL = Selenide.$x("//div[@id='deleteConfirmationModal']"); - private final SelenideElement CONFIRMATION_INPUT = Selenide.$x("//input[@id='deleteConfirmationInput']"); - private final SelenideElement CONFIRM_DELETION_BUTTON = Selenide.$x("//button[@id='confirmDeletionButton']"); - private final SelenideElement CANCEL_BUTTON = Selenide.$x("//button[contains(@class, 'btn-secondary')]"); - private final SelenideElement GLOBAL_MESSAGE = Selenide.$x("//div[@id='globalMessage']"); - private final SelenideElement GLOBAL_ERROR = Selenide.$x("//div[@id='globalError']"); - - public DeleteAccountPage(String url) { - Selenide.open(url); - } - - public DeleteAccountPage() { - // For navigation to this page from other pages - } - - /** - * Start the account deletion process by clicking the delete button - */ - public DeleteAccountPage startDeletion() { - DELETE_ACCOUNT_BUTTON.click(); - // Wait for modal to appear - DELETE_CONFIRMATION_MODAL.should(appear); - return this; - } - - /** - * Complete account deletion with confirmation - */ - public DeleteAccountPage confirmDeletion(String confirmationText) { - CONFIRMATION_INPUT.setValue(confirmationText); - CONFIRM_DELETION_BUTTON.click(); - return this; - } - - /** - * Complete account deletion with correct confirmation text - */ - public DeleteAccountPage confirmDeletion() { - return confirmDeletion("DELETE"); - } - - /** - * Cancel the deletion process - */ - public DeleteAccountPage cancelDeletion() { - CANCEL_BUTTON.click(); - // Wait for modal to disappear - DELETE_CONFIRMATION_MODAL.should(disappear); - return this; - } - - /** - * Get success message after deletion - */ - public String getSuccessMessage() { - return GLOBAL_MESSAGE.shouldBe(visible).text(); - } - - /** - * Get error message - */ - public String getErrorMessage() { - return GLOBAL_ERROR.shouldBe(visible).text(); - } - - /** - * Check if deletion was successful - */ - public boolean isDeletionSuccessful() { - return GLOBAL_MESSAGE.has(cssClass("alert-success")); - } - - /** - * Check if there's an error message - */ - public boolean hasError() { - return GLOBAL_ERROR.exists() && GLOBAL_ERROR.isDisplayed(); - } - - /** - * Check if the confirmation modal is visible - */ - public boolean isConfirmationModalVisible() { - return DELETE_CONFIRMATION_MODAL.has(cssClass("show")); - } - - /** - * Check if the delete form is hidden (after successful deletion) - */ - public boolean isDeleteFormHidden() { - SelenideElement form = Selenide.$x("//form[@id='deleteAccountForm']"); - return form.has(cssClass("d-none")); - } - - /** - * Wait for page to load completely - */ - public DeleteAccountPage waitForPageLoad() { - DELETE_ACCOUNT_BUTTON.should(appear); - return this; - } - - /** - * Clear confirmation input - */ - public DeleteAccountPage clearConfirmationInput() { - CONFIRMATION_INPUT.clear(); - return this; - } - - /** - * Get the current confirmation input value - */ - public String getConfirmationInputValue() { - return CONFIRMATION_INPUT.getValue(); - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/ForgotPasswordPage.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/ForgotPasswordPage.java deleted file mode 100644 index ed3d020..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/ForgotPasswordPage.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui.page; - -import static com.codeborne.selenide.Selenide.$; -import static com.codeborne.selenide.Selenide.$x; -import org.openqa.selenium.By; -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; -import com.digitalsanctuary.spring.demo.user.ui.BaseUiTest; - -public class ForgotPasswordPage extends BaseUiTest { - private final SelenideElement EMAIL_FIELD = $(By.id("email")); - private final SelenideElement SUBMIT_BTN = $x("//button"); - - public ForgotPasswordPage(String url) { - Selenide.open(url); - } - - public ForgotPasswordPage fillEmail(String email) { - EMAIL_FIELD.setValue(email); - return this; - } - - public SuccessResetPasswordPage clickSubmitBtn() { - SUBMIT_BTN.click(); - return new SuccessResetPasswordPage(); - } - -} diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/LoginPage.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/LoginPage.java deleted file mode 100644 index 83b074a..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/LoginPage.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui.page; - -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; - -public class LoginPage { - - private final SelenideElement EMAIL_FIELD = Selenide.$x("//input[@id='username']"); - - private final SelenideElement PASSWORD_FIELD = Selenide.$x("//input[@id='password']"); - - private final SelenideElement LOGIN_BUTTON = Selenide.$x("//button[@id='loginButton']"); - - public LoginPage(String url) { - Selenide.open(url); - } - - public LoginSuccessPage signIn(String email, String password) { - EMAIL_FIELD.setValue(email); - PASSWORD_FIELD.setValue(password); - LOGIN_BUTTON.click(); - return new LoginSuccessPage(); - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/LoginSuccessPage.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/LoginSuccessPage.java deleted file mode 100644 index 98f6720..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/LoginSuccessPage.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui.page; - -import com.codeborne.selenide.ElementsCollection; -import com.codeborne.selenide.Selenide; - -public class LoginSuccessPage { - - private final ElementsCollection WELCOME_MESSAGE = Selenide.$$x("//section//div//span"); - - public String welcomeMessage() { - return String.format("%s %s", WELCOME_MESSAGE.get(0).text(), WELCOME_MESSAGE.get(1).text()); - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/RegisterPage.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/RegisterPage.java deleted file mode 100644 index d8adbd4..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/RegisterPage.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui.page; - -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; - -/** - * Register page - */ -public class RegisterPage { - - private final SelenideElement FIRST_NAME_FIELD = Selenide.$x("//input[@id='firstName']"); - - private final SelenideElement LAST_NAME_FIELD = Selenide.$x("//input[@id='lastName']"); - - private final SelenideElement EMAIL_FIELD = Selenide.$x("//input[@id='email']"); - - private final SelenideElement PASSWORD_FIELD = Selenide.$x("//input[@id='password']"); - - private final SelenideElement CONFIRM_PASSWORD_FIELD = Selenide.$x("//input[@id='matchPassword']"); - - private final SelenideElement SIGN_UP_BUTTON = Selenide.$x("//button[@id='signUpButton']"); - - private final SelenideElement ACCOUNT_EXIST_ERROR_MESSAGE = Selenide.$x("//div[@id='existingAccountError']//span"); - - public RegisterPage(String url) { - Selenide.open(url); - } - - /** - * Filling register form and click signUp button - */ - public SuccessRegisterPage signUp(String firstName, String lastName, String email, String password, String confirmPassword) { - FIRST_NAME_FIELD.setValue(firstName); - LAST_NAME_FIELD.setValue(lastName); - EMAIL_FIELD.setValue(email); - PASSWORD_FIELD.setValue(password); - CONFIRM_PASSWORD_FIELD.setValue(confirmPassword); - SIGN_UP_BUTTON.click(); - return new SuccessRegisterPage(); - } - - public String accountExistErrorMessage() { - return ACCOUNT_EXIST_ERROR_MESSAGE.text(); - } - -} diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/SuccessRegisterPage.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/SuccessRegisterPage.java deleted file mode 100644 index 816f68e..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/SuccessRegisterPage.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui.page; - -import com.codeborne.selenide.SelenideElement; - -import static com.codeborne.selenide.Selenide.$x; - -public class SuccessRegisterPage { - private final SelenideElement SUCCESS_MESSAGE = $x("//section//div//h1"); - - public String message() { - return SUCCESS_MESSAGE.text(); - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/SuccessResetPasswordPage.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/SuccessResetPasswordPage.java deleted file mode 100644 index 54671ed..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/SuccessResetPasswordPage.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui.page; - -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; -import com.digitalsanctuary.spring.demo.user.ui.BaseUiTest; - -public class SuccessResetPasswordPage extends BaseUiTest { - private final SelenideElement SUCCESS_RESET_MESSAGE = Selenide.$x("//div[@class='container']//div[@class='container']//span"); - - public String message() { - return SUCCESS_RESET_MESSAGE.text(); - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/UpdatePasswordPage.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/UpdatePasswordPage.java deleted file mode 100644 index a66282a..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/UpdatePasswordPage.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui.page; - -import static com.codeborne.selenide.Condition.appear; -import static com.codeborne.selenide.Condition.cssClass; -import static com.codeborne.selenide.Condition.visible; -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; - -/** - * Page object for update password page - */ -public class UpdatePasswordPage { - - private final SelenideElement CURRENT_PASSWORD_FIELD = Selenide.$x("//input[@id='oldPassword']"); - private final SelenideElement NEW_PASSWORD_FIELD = Selenide.$x("//input[@id='password']"); - private final SelenideElement CONFIRM_PASSWORD_FIELD = Selenide.$x("//input[@id='matchPassword']"); - private final SelenideElement UPDATE_PASSWORD_BUTTON = Selenide.$x("//button[@type='submit']"); - private final SelenideElement GLOBAL_MESSAGE = Selenide.$x("//div[@id='globalMessage']"); - private final SelenideElement BACK_TO_PROFILE_LINK = Selenide.$x("//a[contains(@href, 'update-user')]"); - - public UpdatePasswordPage(String url) { - Selenide.open(url); - } - - public UpdatePasswordPage() { - // For navigation to this page from other pages - } - - /** - * Update user password - */ - public UpdatePasswordPage updatePassword(String currentPassword, String newPassword, String confirmPassword) { - CURRENT_PASSWORD_FIELD.setValue(currentPassword); - NEW_PASSWORD_FIELD.setValue(newPassword); - CONFIRM_PASSWORD_FIELD.setValue(confirmPassword); - UPDATE_PASSWORD_BUTTON.click(); - - // Wait for response message - GLOBAL_MESSAGE.should(appear); - return this; - } - - /** - * Update password with matching new password - */ - public UpdatePasswordPage updatePassword(String currentPassword, String newPassword) { - return updatePassword(currentPassword, newPassword, newPassword); - } - - /** - * Get the success/error message displayed after password update - */ - public String getMessage() { - return GLOBAL_MESSAGE.shouldBe(visible).text(); - } - - /** - * Check if password update was successful - */ - public boolean isPasswordUpdateSuccessful() { - return GLOBAL_MESSAGE.has(cssClass("alert-success")); - } - - /** - * Check if there's a password mismatch error - */ - public boolean hasPasswordMismatchError() { - SelenideElement matchPasswordError = CONFIRM_PASSWORD_FIELD.parent().$(".form-text.text-danger"); - return matchPasswordError.exists() && matchPasswordError.isDisplayed(); - } - - /** - * Navigate back to profile page - */ - public UpdateUserPage goBackToProfile() { - BACK_TO_PROFILE_LINK.click(); - return new UpdateUserPage(); - } - - /** - * Wait for page to load completely - */ - public UpdatePasswordPage waitForPageLoad() { - CURRENT_PASSWORD_FIELD.should(appear); - NEW_PASSWORD_FIELD.should(appear); - CONFIRM_PASSWORD_FIELD.should(appear); - UPDATE_PASSWORD_BUTTON.should(appear); - return this; - } - - /** - * Clear all form fields - */ - public UpdatePasswordPage clearAllFields() { - CURRENT_PASSWORD_FIELD.clear(); - NEW_PASSWORD_FIELD.clear(); - CONFIRM_PASSWORD_FIELD.clear(); - return this; - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/UpdateUserPage.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/UpdateUserPage.java deleted file mode 100644 index 2b23607..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/UpdateUserPage.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui.page; - -import static com.codeborne.selenide.Condition.appear; -import static com.codeborne.selenide.Condition.cssClass; -import static com.codeborne.selenide.Condition.visible; -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; - -/** - * Page object for update user profile page - */ -public class UpdateUserPage { - - private final SelenideElement FIRST_NAME_FIELD = Selenide.$x("//input[@id='firstName']"); - private final SelenideElement LAST_NAME_FIELD = Selenide.$x("//input[@id='lastName']"); - private final SelenideElement UPDATE_BUTTON = Selenide.$x("//button[@type='submit']"); - private final SelenideElement GLOBAL_MESSAGE = Selenide.$x("//div[@id='globalMessage']"); - private final SelenideElement CHANGE_PASSWORD_LINK = Selenide.$x("//a[@href='/user/update-password.html']"); - private final SelenideElement DELETE_ACCOUNT_LINK = Selenide.$x("//a[@href='/user/delete-account.html']"); - - public UpdateUserPage(String url) { - Selenide.open(url); - } - - public UpdateUserPage() { - // For navigation to this page from other pages - } - - /** - * Update user profile information - */ - public UpdateUserPage updateProfile(String firstName, String lastName) { - FIRST_NAME_FIELD.clear(); - FIRST_NAME_FIELD.setValue(firstName); - LAST_NAME_FIELD.clear(); - LAST_NAME_FIELD.setValue(lastName); - UPDATE_BUTTON.click(); - - // Wait for success message to appear - GLOBAL_MESSAGE.should(appear); - return this; - } - - /** - * Get the success/error message displayed after update - */ - public String getMessage() { - return GLOBAL_MESSAGE.shouldBe(visible).text(); - } - - /** - * Check if update was successful - */ - public boolean isUpdateSuccessful() { - return GLOBAL_MESSAGE.has(cssClass("alert-success")); - } - - /** - * Navigate to change password page - */ - public UpdatePasswordPage goToChangePassword() { - CHANGE_PASSWORD_LINK.click(); - return new UpdatePasswordPage(); - } - - /** - * Navigate to delete account page - */ - public DeleteAccountPage goToDeleteAccount() { - DELETE_ACCOUNT_LINK.click(); - return new DeleteAccountPage(); - } - - /** - * Get current first name value - */ - public String getFirstName() { - return FIRST_NAME_FIELD.getValue(); - } - - /** - * Get current last name value - */ - public String getLastName() { - return LAST_NAME_FIELD.getValue(); - } - - /** - * Wait for page to load completely - */ - public UpdateUserPage waitForPageLoad() { - FIRST_NAME_FIELD.should(appear); - LAST_NAME_FIELD.should(appear); - UPDATE_BUTTON.should(appear); - return this; - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/VerificationPendingPage.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/VerificationPendingPage.java deleted file mode 100644 index 2d5e700..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/page/VerificationPendingPage.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui.page; - -import static com.codeborne.selenide.Condition.appear; -import static com.codeborne.selenide.Condition.visible; -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; - -/** - * Page object for registration pending verification page - */ -public class VerificationPendingPage { - - private final SelenideElement VERIFICATION_MESSAGE = Selenide.$x("//div[contains(@class, 'alert')]"); - private final SelenideElement RESEND_VERIFICATION_LINK = Selenide.$x("//a[contains(@href, 'resend')]"); - private final SelenideElement LOGIN_LINK = Selenide.$x("//a[contains(@href, 'login')]"); - - public VerificationPendingPage(String url) { - Selenide.open(url); - } - - public VerificationPendingPage() { - // For navigation to this page from other pages - } - - /** - * Get the verification pending message - */ - public String getVerificationMessage() { - return VERIFICATION_MESSAGE.shouldBe(visible).text(); - } - - /** - * Click the resend verification email link - */ - public VerificationPendingPage resendVerificationEmail() { - RESEND_VERIFICATION_LINK.click(); - return this; - } - - /** - * Navigate to login page - */ - public LoginPage goToLogin() { - LOGIN_LINK.click(); - return new LoginPage("http://localhost:8080/user/login.html"); - } - - /** - * Check if the verification message is displayed - */ - public boolean isVerificationMessageDisplayed() { - return VERIFICATION_MESSAGE.exists() && VERIFICATION_MESSAGE.isDisplayed(); - } - - /** - * Check if resend verification link is available - */ - public boolean isResendLinkAvailable() { - return RESEND_VERIFICATION_LINK.exists() && RESEND_VERIFICATION_LINK.isDisplayed(); - } - - /** - * Wait for page to load completely - */ - public VerificationPendingPage waitForPageLoad() { - VERIFICATION_MESSAGE.should(appear); - return this; - } - - /** - * Simulate email verification by constructing verification URL This method simulates clicking the verification link from email - */ - public LoginSuccessPage simulateEmailVerification(String verificationToken) { - String verificationUrl = "http://localhost:8080/user/registrationConfirm?token=" + verificationToken; - Selenide.open(verificationUrl); - return new LoginSuccessPage(); - } - - /** - * Simulate email verification with an invalid token - */ - public VerificationPendingPage simulateInvalidEmailVerification() { - String verificationUrl = "http://localhost:8080/user/registrationConfirm?token=invalid-token"; - Selenide.open(verificationUrl); - return this; - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/util/DatabaseStateValidator.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/util/DatabaseStateValidator.java deleted file mode 100644 index a810435..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/util/DatabaseStateValidator.java +++ /dev/null @@ -1,215 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui.util; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; - -/** - * Utility class for validating database state during UI tests - */ -public class DatabaseStateValidator { - - private static final String USER_EXISTS_QUERY = - "SELECT COUNT(*) FROM user_account WHERE email = ?"; - - private static final String USER_ENABLED_QUERY = - "SELECT enabled FROM user_account WHERE email = ?"; - - private static final String USER_LOCKED_QUERY = - "SELECT locked FROM user_account WHERE email = ?"; - - private static final String USER_DETAILS_QUERY = - "SELECT first_name, last_name, enabled, locked, failed_login_attempts FROM user_account WHERE email = ?"; - - private static final String VERIFICATION_TOKEN_EXISTS_QUERY = - "SELECT COUNT(*) FROM verification_token WHERE user_id = (SELECT id FROM user_account WHERE email = ?)"; - - private static final String PASSWORD_RESET_TOKEN_EXISTS_QUERY = - "SELECT COUNT(*) FROM password_reset_token WHERE user_id = (SELECT id FROM user_account WHERE email = ?)"; - - /** - * Check if a user exists in the database - */ - public static boolean userExists(String userEmail) { - try (Connection connection = com.digitalsanctuary.spring.user.jdbc.ConnectionManager.open()) { - PreparedStatement statement = connection.prepareStatement(USER_EXISTS_QUERY); - statement.setString(1, userEmail); - ResultSet resultSet = statement.executeQuery(); - - return resultSet.next() && resultSet.getInt(1) > 0; - } catch (SQLException e) { - throw new RuntimeException("Failed to check if user exists: " + userEmail, e); - } - } - - /** - * Check if a user is enabled - */ - public static boolean isUserEnabled(String userEmail) { - try (Connection connection = com.digitalsanctuary.spring.user.jdbc.ConnectionManager.open()) { - PreparedStatement statement = connection.prepareStatement(USER_ENABLED_QUERY); - statement.setString(1, userEmail); - ResultSet resultSet = statement.executeQuery(); - - return resultSet.next() && resultSet.getBoolean(1); - } catch (SQLException e) { - throw new RuntimeException("Failed to check if user is enabled: " + userEmail, e); - } - } - - /** - * Check if a user account is locked - */ - public static boolean isUserLocked(String userEmail) { - try (Connection connection = com.digitalsanctuary.spring.user.jdbc.ConnectionManager.open()) { - PreparedStatement statement = connection.prepareStatement(USER_LOCKED_QUERY); - statement.setString(1, userEmail); - ResultSet resultSet = statement.executeQuery(); - - return resultSet.next() && resultSet.getBoolean(1); - } catch (SQLException e) { - throw new RuntimeException("Failed to check if user is locked: " + userEmail, e); - } - } - - /** - * Get user details for validation - */ - public static UserDetails getUserDetails(String userEmail) { - try (Connection connection = com.digitalsanctuary.spring.user.jdbc.ConnectionManager.open()) { - PreparedStatement statement = connection.prepareStatement(USER_DETAILS_QUERY); - statement.setString(1, userEmail); - ResultSet resultSet = statement.executeQuery(); - - if (resultSet.next()) { - return new UserDetails( - resultSet.getString("first_name"), - resultSet.getString("last_name"), - resultSet.getBoolean("enabled"), - resultSet.getBoolean("locked"), - resultSet.getInt("failed_login_attempts") - ); - } - return null; - } catch (SQLException e) { - throw new RuntimeException("Failed to get user details: " + userEmail, e); - } - } - - /** - * Check if a verification token exists for the user - */ - public static boolean hasVerificationToken(String userEmail) { - try (Connection connection = com.digitalsanctuary.spring.user.jdbc.ConnectionManager.open()) { - PreparedStatement statement = connection.prepareStatement(VERIFICATION_TOKEN_EXISTS_QUERY); - statement.setString(1, userEmail); - ResultSet resultSet = statement.executeQuery(); - - return resultSet.next() && resultSet.getInt(1) > 0; - } catch (SQLException e) { - throw new RuntimeException("Failed to check verification token: " + userEmail, e); - } - } - - /** - * Check if a password reset token exists for the user - */ - public static boolean hasPasswordResetToken(String userEmail) { - try (Connection connection = com.digitalsanctuary.spring.user.jdbc.ConnectionManager.open()) { - PreparedStatement statement = connection.prepareStatement(PASSWORD_RESET_TOKEN_EXISTS_QUERY); - statement.setString(1, userEmail); - ResultSet resultSet = statement.executeQuery(); - - return resultSet.next() && resultSet.getInt(1) > 0; - } catch (SQLException e) { - throw new RuntimeException("Failed to check password reset token: " + userEmail, e); - } - } - - /** - * Validate complete user registration state - */ - public static void validateUserRegistered(String userEmail, String firstName, String lastName) { - if (!userExists(userEmail)) { - throw new AssertionError("User should exist after registration: " + userEmail); - } - - UserDetails details = getUserDetails(userEmail); - if (!details.getFirstName().equals(firstName)) { - throw new AssertionError("First name mismatch. Expected: " + firstName + ", Actual: " + details.getFirstName()); - } - - if (!details.getLastName().equals(lastName)) { - throw new AssertionError("Last name mismatch. Expected: " + lastName + ", Actual: " + details.getLastName()); - } - - if (!hasVerificationToken(userEmail)) { - throw new AssertionError("Verification token should exist after registration: " + userEmail); - } - } - - /** - * Validate user email verification completed - */ - public static void validateEmailVerified(String userEmail) { - if (!isUserEnabled(userEmail)) { - throw new AssertionError("User should be enabled after email verification: " + userEmail); - } - - if (hasVerificationToken(userEmail)) { - throw new AssertionError("Verification token should be consumed after verification: " + userEmail); - } - } - - /** - * Validate user profile update - */ - public static void validateProfileUpdated(String userEmail, String newFirstName, String newLastName) { - UserDetails details = getUserDetails(userEmail); - if (!details.getFirstName().equals(newFirstName)) { - throw new AssertionError("First name not updated. Expected: " + newFirstName + ", Actual: " + details.getFirstName()); - } - - if (!details.getLastName().equals(newLastName)) { - throw new AssertionError("Last name not updated. Expected: " + newLastName + ", Actual: " + details.getLastName()); - } - } - - /** - * Validate user account deletion - */ - public static void validateAccountDeleted(String userEmail) { - if (userExists(userEmail)) { - // Could be soft delete - check if disabled instead - if (isUserEnabled(userEmail)) { - throw new AssertionError("User account should be deleted or disabled: " + userEmail); - } - } - } - - /** - * Data class for user details - */ - public static class UserDetails { - private final String firstName; - private final String lastName; - private final boolean enabled; - private final boolean locked; - private final int failedLoginAttempts; - - public UserDetails(String firstName, String lastName, boolean enabled, boolean locked, int failedLoginAttempts) { - this.firstName = firstName; - this.lastName = lastName; - this.enabled = enabled; - this.locked = locked; - this.failedLoginAttempts = failedLoginAttempts; - } - - public String getFirstName() { return firstName; } - public String getLastName() { return lastName; } - public boolean isEnabled() { return enabled; } - public boolean isLocked() { return locked; } - public int getFailedLoginAttempts() { return failedLoginAttempts; } - } -} \ No newline at end of file diff --git a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/util/EmailVerificationSimulator.java b/src/test/java/com/digitalsanctuary/spring/demo/user/ui/util/EmailVerificationSimulator.java deleted file mode 100644 index f019c1d..0000000 --- a/src/test/java/com/digitalsanctuary/spring/demo/user/ui/util/EmailVerificationSimulator.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.digitalsanctuary.spring.demo.user.ui.util; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; - -/** - * Utility class for simulating email verification in UI tests - */ -public class EmailVerificationSimulator { - - private static final String GET_VERIFICATION_TOKEN_QUERY = - "SELECT token FROM verification_token WHERE user_id = (SELECT id FROM user_account WHERE email = ?)"; - - private static final String ENABLE_USER_QUERY = "UPDATE user_account SET enabled = true WHERE email = ?"; - - private static final String DELETE_VERIFICATION_TOKEN_QUERY = - "DELETE FROM verification_token WHERE user_id = (SELECT id FROM user_account WHERE email = ?)"; - - /** - * Get verification token for a user by email In a real application, this would come from the email content - */ - public static String getVerificationToken(String userEmail) { - try (Connection connection = com.digitalsanctuary.spring.user.jdbc.ConnectionManager.open()) { - PreparedStatement statement = connection.prepareStatement(GET_VERIFICATION_TOKEN_QUERY); - statement.setString(1, userEmail); - ResultSet resultSet = statement.executeQuery(); - - if (resultSet.next()) { - return resultSet.getString(1); - } - return null; - } catch (SQLException e) { - throw new RuntimeException("Failed to get verification token for " + userEmail, e); - } - } - - /** - * Simulate email verification by directly enabling the user This bypasses the actual email verification endpoint - */ - public static void simulateEmailVerification(String userEmail) { - try (Connection connection = com.digitalsanctuary.spring.user.jdbc.ConnectionManager.open()) { - // Enable the user - PreparedStatement enableStatement = connection.prepareStatement(ENABLE_USER_QUERY); - enableStatement.setString(1, userEmail); - enableStatement.executeUpdate(); - - // Delete the verification token (consumed) - PreparedStatement deleteStatement = connection.prepareStatement(DELETE_VERIFICATION_TOKEN_QUERY); - deleteStatement.setString(1, userEmail); - deleteStatement.executeUpdate(); - - } catch (SQLException e) { - throw new RuntimeException("Failed to simulate email verification for " + userEmail, e); - } - } - - /** - * Check if user has a pending verification token - */ - public static boolean hasVerificationToken(String userEmail) { - return getVerificationToken(userEmail) != null; - } - - /** - * Generate a mock verification URL for testing - */ - public static String generateVerificationUrl(String userEmail) { - String token = getVerificationToken(userEmail); - if (token == null) { - throw new RuntimeException("No verification token found for user: " + userEmail); - } - return "http://localhost:8080/user/registrationConfirm?token=" + token; - } - - /** - * Generate an invalid verification URL for testing error scenarios - */ - public static String generateInvalidVerificationUrl() { - return "http://localhost:8080/user/registrationConfirm?token=invalid-token-12345"; - } - - /** - * Get verification token directly from database (for testing purposes) This simulates what would normally be extracted from the email content - */ - public static String extractTokenFromEmail(String userEmail) { - // In a real test, this would parse the email content - // For our simulation, we get it directly from the database - return getVerificationToken(userEmail); - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/user/api/data/ApiTestData.java b/src/test/java/com/digitalsanctuary/spring/user/api/data/ApiTestData.java index 01156ae..71079fb 100644 --- a/src/test/java/com/digitalsanctuary/spring/user/api/data/ApiTestData.java +++ b/src/test/java/com/digitalsanctuary/spring/user/api/data/ApiTestData.java @@ -2,12 +2,10 @@ import com.digitalsanctuary.spring.user.dto.PasswordDto; import com.digitalsanctuary.spring.user.dto.UserDto; -import com.digitalsanctuary.spring.user.service.DSUserDetails; public class ApiTestData { public static final UserDto BASE_TEST_USER = getUserDto(); - public static final DSUserDetails DEFAULT_DETAILS = new DSUserDetails(null, null); public static PasswordDto getPasswordDto() { PasswordDto dto = new PasswordDto(); diff --git a/src/test/java/com/digitalsanctuary/spring/user/jdbc/ConnectionManager.java b/src/test/java/com/digitalsanctuary/spring/user/jdbc/ConnectionManager.java deleted file mode 100644 index 4f8e9ed..0000000 --- a/src/test/java/com/digitalsanctuary/spring/user/jdbc/ConnectionManager.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.digitalsanctuary.spring.user.jdbc; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; - -public class ConnectionManager { - - private static final String driver = "org.mariadb.jdbc.Driver"; - - private static final String url = "jdbc:mariadb://127.0.0.1:3306/springuser"; - - private static final String username = "springuser"; - - private static final String password = "springuser"; - - static { - initDriver(); - } - - private static void initDriver() { - try { - Class.forName(driver); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - } - - public static Connection open() { - try { - return DriverManager.getConnection(url, username, password); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - -} diff --git a/src/test/java/com/digitalsanctuary/spring/user/jdbc/Jdbc.java b/src/test/java/com/digitalsanctuary/spring/user/jdbc/Jdbc.java deleted file mode 100644 index f443ed5..0000000 --- a/src/test/java/com/digitalsanctuary/spring/user/jdbc/Jdbc.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.digitalsanctuary.spring.user.jdbc; - -import static com.digitalsanctuary.spring.demo.user.ui.data.UiTestData.TEST_USER_ENCODED_PASSWORD; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import com.digitalsanctuary.spring.user.dto.UserDto; - -/** - * Using for delete/save user test data - */ -public class Jdbc { - private static final String DELETE_VERIFICATION_TOKEN_QUERY = - "DELETE FROM verification_token WHERE user_id = (SELECT id FROM user_account WHERE email = ?)"; - private static final String DELETE_TEST_USER_ROLE = "DELETE FROM users_roles WHERE user_id = (SELECT id FROM user_account WHERE email = ?)"; - private static final String DELETE_TEST_PASSWORD_RESET_TOKEN = - "DELETE FROM password_reset_token WHERE user_id = (SELECT id FROM user_account WHERE email = ?)"; - private static final String DELETE_TEST_USER_QUERY = "DELETE FROM user_account WHERE email = ?"; - private static final String GET_LAST_USER_ID_QUERY = "SELECT max(id) FROM user_account"; - private static final String GET_PASSWORD_RESET_TOKEN_BY_USER_EMAIL_QUERY = - "SELECT token FROM password_reset_token WHERE user_id = (SELECT id FROM user_account WHERE email = ?)"; - - private static final String SAVE_TEST_USER_QUERY = "INSERT INTO user_account (id, first_name, last_name, email, " - + "password, enabled, failed_login_attempts, locked) VALUES (?,?,?,?,?,?,?,?)"; - - public static void deleteTestUser(UserDto userDto) { - try (Connection connection = ConnectionManager.open()) { - String[] params = new String[] {userDto.getEmail()}; - execute(connection, DELETE_VERIFICATION_TOKEN_QUERY, params); - execute(connection, DELETE_TEST_USER_ROLE, params); - execute(connection, DELETE_TEST_PASSWORD_RESET_TOKEN, params); - execute(connection, DELETE_TEST_USER_QUERY, params); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - - public static void saveTestUser(UserDto userDto) { - try (Connection connection = ConnectionManager.open()) { - ResultSet resultSet = connection.prepareStatement(GET_LAST_USER_ID_QUERY).executeQuery(); - int id = 0; - if (resultSet.next()) { - id = (resultSet.getInt(1) + 1); - } - Object[] params = - new Object[] {id, userDto.getFirstName(), userDto.getLastName(), userDto.getEmail(), TEST_USER_ENCODED_PASSWORD, true, 0, false}; - execute(connection, SAVE_TEST_USER_QUERY, params); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - - private static void execute(Connection connection, String query, Object[] params) throws SQLException { - PreparedStatement statement = connection.prepareStatement(query); - for (int i = 0; i < params.length; i++) { - Object param = params[i]; - if (param instanceof Integer) { - statement.setInt((i + 1), (Integer) param); - } - if (param instanceof String) { - statement.setString((i + 1), (String) param); - } - if (param instanceof Boolean) { - statement.setBoolean((i + 1), (Boolean) param); - } - } - statement.executeUpdate(); - } - - public static String getPasswordRestTokenByUserEmail(String email) { - String token = ""; - try (Connection connection = ConnectionManager.open()) { - PreparedStatement statement = connection.prepareStatement(GET_PASSWORD_RESET_TOKEN_BY_USER_EMAIL_QUERY); - statement.setString(1, email); - ResultSet set = statement.executeQuery(); - if (set.next()) { - token = set.getString(1); - } - } catch (SQLException e) { - throw new RuntimeException(e); - } - return token; - } -} diff --git a/src/test/java/com/digitalsanctuary/spring/user/security/EmailVerificationEdgeCaseTest.java b/src/test/java/com/digitalsanctuary/spring/user/security/EmailVerificationEdgeCaseTest.java index 76a61f3..b9e6923 100644 --- a/src/test/java/com/digitalsanctuary/spring/user/security/EmailVerificationEdgeCaseTest.java +++ b/src/test/java/com/digitalsanctuary/spring/user/security/EmailVerificationEdgeCaseTest.java @@ -35,7 +35,6 @@ import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import com.digitalsanctuary.spring.demo.UserDemoApplication; -import com.digitalsanctuary.spring.demo.user.ui.util.DatabaseStateValidator; import com.digitalsanctuary.spring.user.persistence.model.User; import com.digitalsanctuary.spring.user.persistence.model.VerificationToken; import com.digitalsanctuary.spring.user.persistence.repository.PasswordHistoryRepository; @@ -133,7 +132,7 @@ void testExpiredTokenRejection() throws Exception { .andExpect(status().isOk()).andReturn(); // Verify user remains disabled - assertThat(DatabaseStateValidator.isUserEnabled(testEmail)).isFalse(); + assertThat(userRepository.findByEmail(testEmail).isEnabled()).isFalse(); // Verify token was cleaned up assertThat(verificationTokenRepository.findByToken(expiredToken.getToken())).isNull(); @@ -158,7 +157,7 @@ void testJustExpiredToken() throws Exception { .andExpect(status().isOk()); // Verify user remains disabled - assertThat(DatabaseStateValidator.isUserEnabled(testEmail)).isFalse(); + assertThat(userRepository.findByEmail(testEmail).isEnabled()).isFalse(); // Verify token was cleaned up assertThat(verificationTokenRepository.findByToken(justExpiredToken.getToken())).isNull(); @@ -179,7 +178,7 @@ void testTokenNearExpiry() throws Exception { .andExpect(status().is3xxRedirection()); // Successful verification redirects // Verify user is enabled - assertThat(DatabaseStateValidator.isUserEnabled(testEmail)).isTrue(); + assertThat(userRepository.findByEmail(testEmail).isEnabled()).isTrue(); // Verify token was consumed assertThat(verificationTokenRepository.findByToken(nearExpiryToken.getToken())).isNull(); @@ -210,7 +209,7 @@ void testMultipleTokenRequests() throws Exception { .andExpect(status().is3xxRedirection()); // Verify user is enabled - assertThat(DatabaseStateValidator.isUserEnabled(testEmail)).isTrue(); + assertThat(userRepository.findByEmail(testEmail).isEnabled()).isTrue(); } } @@ -239,7 +238,7 @@ void testInvalidTokenFormats() throws Exception { .andReturn(); // Verify user remains disabled - assertThat(DatabaseStateValidator.isUserEnabled(testEmail)).isFalse(); + assertThat(userRepository.findByEmail(testEmail).isEnabled()).isFalse(); // Verify error message is user-friendly, not a stack trace String responseBody = result.getResponse().getContentAsString(); @@ -277,7 +276,7 @@ void testTamperedTokens() throws Exception { .andExpect(status().isOk()).andReturn(); // Verify user remains disabled - assertThat(DatabaseStateValidator.isUserEnabled(testEmail)).isFalse(); + assertThat(userRepository.findByEmail(testEmail).isEnabled()).isFalse(); // Verify original token still exists (not consumed by tampered attempt) assertThat(verificationTokenRepository.findByToken(originalToken)).isNotNull(); @@ -304,10 +303,10 @@ void testCrossUserTokenAttack() throws Exception { .andExpect(status().isOk()); // Verify our test user remains disabled - assertThat(DatabaseStateValidator.isUserEnabled(testEmail)).isFalse(); + assertThat(userRepository.findByEmail(testEmail).isEnabled()).isFalse(); // Verify other user also remains disabled (token not consumed) - assertThat(DatabaseStateValidator.isUserEnabled(otherEmail)).isFalse(); + assertThat(userRepository.findByEmail(otherEmail).isEnabled()).isFalse(); // Verify token still exists (not consumed) assertThat(verificationTokenRepository.findByToken(otherUserToken.getToken())).isNotNull(); @@ -329,7 +328,7 @@ void testAlreadyUsedToken() throws Exception { .andExpect(status().is3xxRedirection()); // Verify user is enabled - assertThat(DatabaseStateValidator.isUserEnabled(testEmail)).isTrue(); + assertThat(userRepository.findByEmail(testEmail).isEnabled()).isTrue(); // Verify token was consumed assertThat(verificationTokenRepository.findByToken(tokenValue)).isNull(); @@ -402,7 +401,7 @@ void testConcurrentTokenValidation() throws Exception { assertThat(failureCount.get()).isEqualTo(threadCount - 1); // Verify user is enabled (successful verification) - assertThat(DatabaseStateValidator.isUserEnabled(testEmail)).isTrue(); + assertThat(userRepository.findByEmail(testEmail).isEnabled()).isTrue(); // Verify token was consumed assertThat(verificationTokenRepository.findByToken(tokenValue)).isNull(); diff --git a/src/test/java/com/digitalsanctuary/spring/user/ui/page/ForgotPasswordPage.java b/src/test/java/com/digitalsanctuary/spring/user/ui/page/ForgotPasswordPage.java deleted file mode 100644 index eae64db..0000000 --- a/src/test/java/com/digitalsanctuary/spring/user/ui/page/ForgotPasswordPage.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.digitalsanctuary.spring.user.ui.page; - -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; -import com.digitalsanctuary.spring.demo.user.ui.BaseUiTest; -import org.openqa.selenium.By; - -import static com.codeborne.selenide.Selenide.$; -import static com.codeborne.selenide.Selenide.$x; - -public class ForgotPasswordPage extends BaseUiTest { - private final SelenideElement EMAIL_FIELD = $(By.id("email")); - private final SelenideElement SUBMIT_BTN = $x("//button"); - - public ForgotPasswordPage(String url) { - Selenide.open(url); - } - - public ForgotPasswordPage fillEmail(String email) { - EMAIL_FIELD.setValue(email); - return this; - } - - public SuccessResetPasswordPage clickSubmitBtn() { - SUBMIT_BTN.click(); - return new SuccessResetPasswordPage(); - } - -} diff --git a/src/test/java/com/digitalsanctuary/spring/user/ui/page/SuccessResetPasswordPage.java b/src/test/java/com/digitalsanctuary/spring/user/ui/page/SuccessResetPasswordPage.java deleted file mode 100644 index 934edb0..0000000 --- a/src/test/java/com/digitalsanctuary/spring/user/ui/page/SuccessResetPasswordPage.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.digitalsanctuary.spring.user.ui.page; - -import com.codeborne.selenide.Selenide; -import com.codeborne.selenide.SelenideElement; -import com.digitalsanctuary.spring.demo.user.ui.BaseUiTest; - -public class SuccessResetPasswordPage extends BaseUiTest { - private final SelenideElement SUCCESS_RESET_MESSAGE = Selenide.$x("//div[@class='container']//div[@class='container']//span"); - - public String message() { - return SUCCESS_RESET_MESSAGE.text(); - } -} diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index 218281d..b84556a 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -33,5 +33,3 @@ logging.level.org.springframework=DEBUG #logging.level.org.springframework.data.repository=DEBUG #logging.level.org.hibernate=DEBUG #logging.level.org.springframework.orm.jpa=DEBUG - -test.browser=edge