|
| 1 | +I have a blog in asp dotnet. recently I added end to end tests with playwright. however, some tests are inexplicably failing. why? what can I do to fix them? as far as I can tell, the system under test works correctly so the tests should pass but they don't. the full code is attached as dump.txt and all the logs are also available to you as attachments. please review thoroughly, word by word, line by line, and give me a complete, comprehensive solution that fixes all my problems. please and thank you |
| 2 | + |
| 3 | +After reviewing your codebase, test logs, and Docker configuration thoroughly, I've identified the root cause of your E2E test failures. The issue is **multifaceted**, involving Docker volume permissions with SELinux that prevent Data Protection keys from persisting, causing immediate authentication cookie validation failures, combined with subtle timing issues in your Playwright tests. |
| 4 | + |
| 5 | +Here's the comprehensive solution: |
| 6 | + |
| 7 | +## Root Cause Analysis |
| 8 | + |
| 9 | +1. **Data Protection Keys Permission Failure**: Your `Program.cs` correctly configures Data Protection to persist keys to `/app/data/keys`, but your Docker volume mount with SELinux labeling (`:Z`) overrides the permissions set in your Dockerfile. The `appuser` cannot write to the mounted volume, causing keys to remain in-memory only. This immediately invalidates authentication cookies right after they're set. |
| 10 | + |
| 11 | +2. **Form Submission Timing**: Playwright's `ClickAsync()` does not inherently wait for full page navigation when submitting forms. Your tests assume the navigation completes before assertions run, but Blazor Server + form submission causes a race condition. |
| 12 | + |
| 13 | +3. **Missing Keys Directory**: Your Dockerfile creates `/app/data` but not the nested `/app/data/keys` directory with explicit permissions. |
| 14 | + |
| 15 | +## The Fix |
| 16 | + |
| 17 | +### 1. Update Your Dockerfile (`src/MyBlog.Web/Dockerfile`) |
| 18 | + |
| 19 | +Add explicit creation and permission setting for the Data Protection keys directory: |
| 20 | + |
| 21 | +```dockerfile |
| 22 | +# Create data directory for SQLite AND Data Protection keys |
| 23 | +RUN mkdir -p /app/data/keys && \ |
| 24 | + chown -R appuser:appuser /app/data && \ |
| 25 | + chmod -R 755 /app/data |
| 26 | + |
| 27 | +# Ensure the appuser owns the keys directory specifically |
| 28 | +USER appuser |
| 29 | +RUN mkdir -p /app/data/keys # Create as appuser to ensure ownership |
| 30 | +``` |
| 31 | + |
| 32 | +### 2. Update Your Login E2E Tests (`src/MyBlog.E2E/Tests/LoginPageTests.cs`) |
| 33 | + |
| 34 | +Replace your form submission logic to explicitly wait for navigation: |
| 35 | + |
| 36 | +```csharp |
| 37 | +[Fact] |
| 38 | +public async Task LoginPage_WithValidCredentials_RedirectsToAdmin() |
| 39 | +{ |
| 40 | + var page = await _fixture.CreatePageAsync(); |
| 41 | + await page.GotoAsync("/login"); |
| 42 | + |
| 43 | + await page.FillAsync("input[name='username']", "admin"); |
| 44 | + await page.FillAsync("input[name='password']", "ChangeMe123!"); |
| 45 | + |
| 46 | + // FIX: Use RunAndWaitForNavigationAsync to ensure form submission completes |
| 47 | + await page.RunAndWaitForNavigationAsync(async () => |
| 48 | + { |
| 49 | + await page.ClickAsync("button[type='submit']"); |
| 50 | + }, new PageRunAndWaitForNavigationOptions |
| 51 | + { |
| 52 | + UrlRegex = new Regex("/admin|/login.*error") |
| 53 | + }); |
| 54 | + |
| 55 | + // Now assert the URL |
| 56 | + await Assertions.Expect(page).ToHaveURLAsync(new Regex("/admin")); |
| 57 | +} |
| 58 | + |
| 59 | +[Fact] |
| 60 | +public async Task LoginPage_WithInvalidCredentials_ShowsError() |
| 61 | +{ |
| 62 | + var page = await _fixture.CreatePageAsync(); |
| 63 | + await page.GotoAsync("/login"); |
| 64 | + |
| 65 | + await page.FillAsync("input[name='username']", "invalid"); |
| 66 | + await page.FillAsync("input[name='password']", "invalid"); |
| 67 | + |
| 68 | + // FIX: Wait for navigation to complete (even if it redirects back to login) |
| 69 | + await page.RunAndWaitForNavigationAsync(async () => |
| 70 | + { |
| 71 | + await page.ClickAsync("button[type='submit']"); |
| 72 | + }); |
| 73 | + |
| 74 | + // Wait specifically for the error query parameter to appear |
| 75 | + await Assertions.Expect(page).ToHaveURLAsync(new Regex("/login.*error=invalid")); |
| 76 | + |
| 77 | + // Now check for the error message |
| 78 | + var errorLocator = page.Locator(".error-message"); |
| 79 | + await Assertions.Expect(errorLocator).ToBeVisibleAsync(); |
| 80 | + await Assertions.Expect(errorLocator).ToContainTextAsync("Invalid username or password"); |
| 81 | +} |
| 82 | + |
| 83 | +[Fact] |
| 84 | +public async Task LoginPage_AfterLogin_ShowsLogoutButton() |
| 85 | +{ |
| 86 | + var page = await _fixture.CreatePageAsync(); |
| 87 | + await page.GotoAsync("/login"); |
| 88 | + |
| 89 | + await page.FillAsync("input[name='username']", "admin"); |
| 90 | + await page.FillAsync("input[name='password']", "ChangeMe123!"); |
| 91 | + |
| 92 | + // FIX: Explicitly wait for navigation to admin page |
| 93 | + await page.RunAndWaitForNavigationAsync(async () => |
| 94 | + { |
| 95 | + await page.ClickAsync("button[type='submit']"); |
| 96 | + }, new PageRunAndWaitForNavigationOptions { UrlRegex = new Regex("/admin") }); |
| 97 | + |
| 98 | + await Assertions.Expect(page).ToHaveURLAsync(new Regex("/admin")); |
| 99 | + await Assertions.Expect(page.Locator("h1")).ToContainTextAsync("Dashboard"); |
| 100 | + |
| 101 | + var logoutButton = page.Locator("form[action='/logout'] button"); |
| 102 | + await Assertions.Expect(logoutButton).ToBeVisibleAsync(); |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +### 3. Add Pre-Test Database Seeding Verification |
| 107 | + |
| 108 | +Add a setup check in your `PlaywrightFixture` to verify the admin user exists before tests run: |
| 109 | + |
| 110 | +```csharp |
| 111 | +// In LoginPageTests.cs or as a shared setup |
| 112 | +[Fact] |
| 113 | +public async Task EnsureAdminUserExists() |
| 114 | +{ |
| 115 | + // This ensures the admin user is created before other tests run |
| 116 | + using var client = new HttpClient { BaseAddress = new Uri(_fixture.BaseUrl) }; |
| 117 | + var response = await client.GetAsync("/login"); |
| 118 | + Assert.True(response.StatusCode == System.Net.HttpStatusCode.OK); |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +### 4. Alternative: Disable Data Protection Persistence for E2E (Optional) |
| 123 | + |
| 124 | +If you want to run tests without worrying about volume permissions, modify `Program.cs` to use ephemeral keys only in Development/E2E environments: |
| 125 | + |
| 126 | +```csharp |
| 127 | +var builder = WebApplication.CreateBuilder(args); |
| 128 | + |
| 129 | +// ... existing code ... |
| 130 | +
|
| 131 | +// Only persist keys in Production; use ephemeral for E2E |
| 132 | +if (builder.Environment.IsProduction()) |
| 133 | +{ |
| 134 | + builder.Services.AddDataProtection() |
| 135 | + .PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(builder.Environment.ContentRootPath, "data", "keys"))); |
| 136 | +} |
| 137 | +// else: use default in-memory keys (sufficient for single-instance E2E tests) |
| 138 | +``` |
| 139 | + |
| 140 | +**However**, I recommend keeping the persistence and fixing the permissions as shown in step 1, as it's closer to production. |
| 141 | + |
| 142 | +### 5. Verify Environment Variable Configuration |
| 143 | + |
| 144 | +Ensure your Docker Compose is correctly passing the admin password (which it appears to be, but verify the exact key): |
| 145 | + |
| 146 | +```yaml |
| 147 | +# In docker-compose.e2e.yml |
| 148 | +services: |
| 149 | + myblog-web: |
| 150 | + environment: |
| 151 | + - Authentication__DefaultAdminPassword=ChangeMe123! |
| 152 | + # Ensure this matches exactly what AuthService expects |
| 153 | +``` |
| 154 | + |
| 155 | +Your `AuthService.cs` looks correct with this fallback chain: |
| 156 | +```csharp |
| 157 | +Environment.GetEnvironmentVariable("MYBLOG_ADMIN_PASSWORD") |
| 158 | + ?? _configuration["Authentication:DefaultAdminPassword"] |
| 159 | + ?? "ChangeMe123!" |
| 160 | +``` |
| 161 | + |
| 162 | +## Verification Steps |
| 163 | + |
| 164 | +After implementing these fixes: |
| 165 | + |
| 166 | +1. **Rebuild containers**: `./run-e2e.sh --clean --build` |
| 167 | +2. **Check logs**: After a test run, check `test-results/myblog-web.log` for any Data Protection errors (permission denied exceptions) |
| 168 | +3. **Run tests**: The navigation assertions should now pass because: |
| 169 | + - The authentication cookie will be valid (Data Protection keys are persisted correctly) |
| 170 | + - The tests wait for navigation to complete before asserting URLs |
| 171 | + - The error message element will be present after the redirect with query parameters |
| 172 | + |
| 173 | +## Additional Considerations |
| 174 | + |
| 175 | +- **Rate Limiting**: Your tests may trigger the rate limiting middleware (`LoginRateLimitMiddleware`) if run multiple times from the same IP (container IP). The tests clear attempts via `Dispose()`, but if tests fail mid-run, subsequent attempts may be delayed. The `RunAndWaitForNavigationAsync` will handle the delay gracefully, but be aware of the 30s max delay if many failed attempts occur. |
| 176 | + |
| 177 | +- **Antiforgery**: Ensure your `Login.razor` includes the `@Html.AntiForgeryToken()` implicitly via `<AntiforgeryToken />` (which it does). With `data-enhance="false"`, this ensures the token is posted to your minimal API endpoint. |
| 178 | + |
| 179 | +- **Container Health**: Your `run-e2e.sh` healthcheck waits for the root page, but you might want to add a check that the database is seeded: |
| 180 | + ```bash |
| 181 | + # Add to run-e2e.sh after healthcheck loop |
| 182 | + echo "Verifying admin user exists..." |
| 183 | + until curl -sf http://localhost:5000/login | grep -q "username"; do |
| 184 | + sleep 1 |
| 185 | + done |
| 186 | + ``` |
| 187 | + |
| 188 | +These changes should resolve all three failing tests by ensuring the authentication flow works correctly within the containerized environment and that Playwright properly synchronizes with the form submission navigation. |
| 189 | + |
| 190 | + |
| 191 | + |
| 192 | + |
| 193 | + |
| 194 | + |
| 195 | + |
| 196 | + |
| 197 | + |
| 198 | + |
| 199 | + |
| 200 | + |
0 commit comments