Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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