Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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 |

Expand Down
14 changes: 1 addition & 13 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand All @@ -110,7 +108,6 @@ dependencies {

test {
useJUnitPlatform {
excludeTags 'ui'
}
testLogging {
events "PASSED", "FAILED", "SKIPPED"
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion playwright/src/pages/BasePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export abstract class BasePage {
* Wait for navigation to complete.
*/
async waitForNavigation(): Promise<void> {
await this.page.waitForLoadState('networkidle');
await this.page.waitForLoadState('domcontentloaded');
}

/**
Expand Down
4 changes: 2 additions & 2 deletions playwright/src/pages/EventDetailsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down
32 changes: 16 additions & 16 deletions playwright/tests/access-control/protected-pages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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');
Expand Down
10 changes: 5 additions & 5 deletions playwright/tests/auth/email-verification.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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');
Expand Down
4 changes: 2 additions & 2 deletions playwright/tests/auth/login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});

Expand Down Expand Up @@ -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();

Expand Down
16 changes: 8 additions & 8 deletions playwright/tests/auth/password-reset.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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!';
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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)
});
Expand Down
Loading
Loading