Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug/401 from the backend during ssr will crash the node server #357

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
8 changes: 8 additions & 0 deletions backend/LexBoxApi/Controllers/AuthTestingController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using LexBoxApi.Auth;
using LexCore.Auth;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace LexBoxApi.Controllers;
Expand Down Expand Up @@ -27,4 +28,11 @@ public OkResult RequiresForgotPasswordAudience()
{
return Ok();
}

[HttpGet("403")]
[AllowAnonymous]
public ForbidResult Forbidden()
{
return Forbid();
}
}
15 changes: 13 additions & 2 deletions backend/Testing/Browser/Base/PageTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ await Context.Tracing.StartAsync(new()
{
DeferredExceptions.Add(new UnexpectedResponseException(response));
}
else if (response.Request.IsNavigationRequest && response.Status >= (int)HttpStatusCode.BadRequest)
{
// 400s are client errors that our tests shouldn't trigger under normal circumstances.
// And if they're navigation requests SvelteKit/our UI might never see them (e.g. /api/*)
// i.e. they won't be handled well i.e. we don't like them.
DeferredExceptions.Add(new UnexpectedResponseException(response));
}
};
}

Expand Down Expand Up @@ -102,18 +109,22 @@ public async Task LoginAs(string user, string password)
.ShouldContainKey("Set-Cookie");
var cookies = responseMessage.Headers.GetValues("Set-Cookie").ToArray();
cookies.ShouldNotBeEmpty();
await SetCookies(cookies);
}

protected async Task SetCookies(string[] cookies)
{
var cookieContainer = new CookieContainer();
foreach (var cookie in cookies)
{
cookieContainer.SetCookies(new($"{TestingEnvironmentVariables.ServerBaseUrl}"), cookie);
}

await Context.AddCookiesAsync(cookieContainer.GetAllCookies()
.Select(cookie => new Microsoft.Playwright.Cookie
{
Value = cookie.Value,
Domain = cookie.Domain,
Expires = (float)cookie.Expires.Subtract(DateTime.UnixEpoch).TotalSeconds,
Expires = cookie.Expires == default ? null : (float)cookie.Expires.Subtract(DateTime.UnixEpoch).TotalSeconds,
Name = cookie.Name,
Path = cookie.Path,
Secure = cookie.Secure,
Expand Down
187 changes: 187 additions & 0 deletions backend/Testing/Browser/ErrorHandlingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
using LexBoxApi.Auth;
using Microsoft.Playwright;
using Shouldly;
using Testing.Browser.Base;
using Testing.Browser.Page;
using Testing.Browser.Page.External;
using Testing.Services;

namespace Testing.Browser;

[Trait("Category", "Integration")]
public class ErrorHandlingTests : PageTest
{
[Fact]
public async Task CatchGoto500InSameTab()
{
await new SandboxPage(Page).Goto();
await Page.RunAndWaitForResponseAsync(async () =>
{
await Page.GetByText("Goto API 500", new() { Exact = true }).ClickAsync();
}, "/api/testing/test500NoException");
ExpectDeferredException();
}

[Fact]
public async Task CatchGoto500InNewTab()
{
await new SandboxPage(Page).Goto();
await Context.RunAndWaitForPageAsync(async () =>
{
await Page.GetByText("Goto API 500 new tab").ClickAsync();
});
ExpectDeferredException();
}

[Fact]
public async Task CatchPageLoad500()
{
await new SandboxPage(Page).Goto();
await Page.GetByText("Goto page load 500").ClickAsync();
ExpectDeferredException();
await Expect(Page.Locator(":text-matches('Unexpected response:.*(500)', 'g')").First).ToBeVisibleAsync();
}

[Fact]
public async Task PageLoad500InNewTabLandsOnErrorPage()
{
await new SandboxPage(Page).Goto();
var newPage = await Context.RunAndWaitForPageAsync(async () =>
{
await Page.GetByText("Goto page load 500").ClickAsync(new()
{
Modifiers = new[] { KeyboardModifier.Control },
});
});
await Expect(newPage.Locator(":text-matches('Unexpected response:.*(500)', 'g')").First).ToBeVisibleAsync();
}

[Fact]
public async Task CatchFetch500AndErrorDialog()
{
await new SandboxPage(Page).Goto();
await Page.RunAndWaitForResponseAsync(async () =>
{
await Page.GetByText("Fetch 500").ClickAsync();
}, "/api/testing/test500NoException");
ExpectDeferredException();
await Expect(Page.Locator(".modal-box.bg-error:text-matches('Unexpected response:.*(500)', 'g')")).ToBeVisibleAsync();
}

[Fact]
public async Task ServerPageLoad403IsRedirectedToLogin()
{
await SetCookies(new[] { $"{AuthKernel.AuthCookieName}={TestConstants.InvalidJwt}" });
await new UserDashboardPage(Page).Goto(new() { ExpectRedirect = true });
await new LoginPage(Page).WaitFor();
}

[Fact]
public async Task ClientPageLoad403IsRedirectedToLogin()
{
await LoginAs("admin", TestingEnvironmentVariables.DefaultPassword);
var adminDashboardPage = await new AdminDashboardPage(Page).Goto();

await SetCookies(new[] { $"{AuthKernel.AuthCookieName}={TestConstants.InvalidJwt}" });

var response = await Page.RunAndWaitForResponseAsync(async () =>
{
await adminDashboardPage.ClickProject("Sena 3");
}, "/api/graphql");

response.Status.ShouldBe(401);
await new LoginPage(Page).WaitFor();
}

[Fact]
public async Task CatchGoto403InSameTab()
{

await new SandboxPage(Page).Goto();
await Page.RunAndWaitForResponseAsync(async () =>
{
await Page.GetByText("Goto API 403", new() { Exact = true }).ClickAsync();
}, "/api/AuthTesting/403");
ExpectDeferredException();
}

[Fact]
public async Task CatchGoto403InNewTab()
{
await new SandboxPage(Page).Goto();
await Context.RunAndWaitForPageAsync(async () =>
{
await Page.GetByText("Goto API 403 new tab").ClickAsync();
});
ExpectDeferredException();
}

[Fact]
public async Task PageLoad403IsRedirectedToHome()
{
await LoginAs("manager", TestingEnvironmentVariables.DefaultPassword);
await new SandboxPage(Page).Goto();
await Page.GetByText("Goto page load 403").ClickAsync();
await new UserDashboardPage(Page).WaitFor();
}

[Fact]
public async Task PageLoad403InNewTabIsRedirectedToHome()
{
await LoginAs("manager", TestingEnvironmentVariables.DefaultPassword);
await new SandboxPage(Page).Goto();
var newPage = await Context.RunAndWaitForPageAsync(async () =>
{
await Page.GetByText("Goto page load 403").ClickAsync(new()
{
Modifiers = new[] { KeyboardModifier.Control },
});
});
await new UserDashboardPage(newPage).WaitFor();
}

[Fact]
public async Task PageLoad403OnHomePageIsRedirectedToLogin()
{
// (1) Get JWT with only forgot-password audience
// - Register
var mailinatorId = Guid.NewGuid().ToString();
var email = $"{mailinatorId}@mailinator.com";
var password = email;
await using var userDashboardPage = await RegisterUser($"Test: {nameof(PageLoad403OnHomePageIsRedirectedToLogin)} - {mailinatorId}", email, password);

// - Request forgot password email
var loginPage = await Logout();
var forgotPasswordPage = await loginPage.ClickForgotPassword();
await forgotPasswordPage.FillForm(email);
await forgotPasswordPage.Submit();

// - Get JWT from reset password link
var inboxPage = await MailInboxPage.Get(Page, mailinatorId).Goto();
var emailPage = await inboxPage.OpenEmail();
var href = await emailPage.ResetPasswordButton.GetAttributeAsync("href");
var forgotPasswordJwt = href.Split("jwt=")[1].Split("&")[0];

// (2) Get to a non-home page with an empty urql cache
await LoginAs(email, password);
var userAccountPage = await new UserAccountSettingsPage(Page).Goto();

// (3) Update cookie with the reset-password audience JWT and try to go home
await SetCookies(new[] { $"{AuthKernel.AuthCookieName}={forgotPasswordJwt}" });

var response = await Page.RunAndWaitForResponseAsync(userAccountPage.GoHome, "/api/graphql");
response.Status.ShouldBe(403);

// (4) Expect to be redirected to login page
await new LoginPage(Page).WaitFor();
}

[Fact]
public async Task NodeSurvivesCorruptJwt()
{
var corruptJwt = "bla-bla-bla";
await SetCookies(new[] { $"{AuthKernel.AuthCookieName}={corruptJwt}" });
await new UserDashboardPage(Page).Goto(new() { ExpectRedirect = true });
await new LoginPage(Page).WaitFor();
}
}
9 changes: 7 additions & 2 deletions backend/Testing/Browser/Page/AdminDashboardPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ public AdminDashboardPage(IPage page)

public async Task<ProjectPage> OpenProject(string projectName, string projectCode)
{
var projectTable = Page.Locator("table").Nth(0);
await projectTable.GetByRole(AriaRole.Link, new() { Name = projectName, Exact = true}).ClickAsync();
await ClickProject(projectName);
return await new ProjectPage(Page, projectName, projectCode).WaitFor();
}

public async Task ClickProject(string projectName)
{
var projectTable = Page.Locator("table").Nth(0);
await projectTable.GetByRole(AriaRole.Link, new() { Name = projectName, Exact = true }).ClickAsync();
}
}
3 changes: 1 addition & 2 deletions backend/Testing/Browser/Page/AuthenticatedBasePage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ public AuthenticatedBasePage(IPage page, string url, ILocator testLocator)
EmailVerificationAlert = new EmailVerificationAlert(Page);
}

public async Task<UserDashboardPage> GoHome()
public async Task GoHome()
{
await Page.Locator(".breadcrumbs").GetByRole(AriaRole.Link, new() { Name = "Home" }).ClickAsync();
return await new UserDashboardPage(Page).WaitFor();
}
}
12 changes: 10 additions & 2 deletions backend/Testing/Browser/Page/BasePage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Testing.Browser.Page;

public record GotoOptions(bool? ExpectRedirect = false);

public abstract class BasePage<T> where T : BasePage<T>
{
public IPage Page { get; private set; }
Expand All @@ -22,7 +24,7 @@ public BasePage(IPage page, string? url, ILocator[] testLocators)
TestLocators = testLocators;
}

public virtual async Task<T> Goto()
public virtual async Task<T> Goto(GotoOptions? options = null)
{
if (Url is null)
{
Expand All @@ -31,7 +33,13 @@ public virtual async Task<T> Goto()

var response = await Page.GotoAsync(Url);
response?.Ok.ShouldBeTrue(); // is null if same URL, but different hash
return await WaitFor();

if (options?.ExpectRedirect != true)
{
await WaitFor();
}

return (T)this;
}

public async Task<T> WaitFor()
Expand Down
4 changes: 2 additions & 2 deletions backend/Testing/Browser/Page/External/MailDevPages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ protected override MailEmailPage GetEmailPage()
return new MailDevEmailPage(Page);
}

public override async Task<MailInboxPage> Goto()
public override async Task<MailInboxPage> Goto(GotoOptions? options = null)
{
await base.Goto();
await base.Goto(options);
await Page.Locator("input.search-input").FillAsync(MailboxId);
return this;
}
Expand Down
4 changes: 3 additions & 1 deletion backend/Testing/Browser/Page/External/MailPages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public abstract class MailEmailPage : BasePage<MailEmailPage>
{
protected readonly ILocator bodyLocator;

public ILocator ResetPasswordButton => bodyLocator.GetByRole(AriaRole.Link, new() { Name = "Reset password" });

public MailEmailPage(IPage page, string? url, ILocator bodyLocator) : base(page, url, bodyLocator)
{
this.bodyLocator = bodyLocator;
Expand All @@ -53,6 +55,6 @@ public Task ClickVerifyEmail()

public Task ClickResetPassword()
{
return bodyLocator.GetByRole(AriaRole.Link, new() { Name = "Reset password" }).ClickAsync();
return ResetPasswordButton.ClickAsync();
}
}
4 changes: 2 additions & 2 deletions backend/Testing/Browser/Page/External/MailinatorPages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ protected override MailEmailPage GetEmailPage()
return new MailinatorEmailPage(Page);
}

public override async Task<MailInboxPage> Goto()
public override async Task<MailInboxPage> Goto(GotoOptions? options = null)
{
Url = $"https://www.mailinator.com/v4/public/inboxes.jsp?to={MailboxId}";
return await base.Goto();
return await base.Goto(options);
}
}

Expand Down
2 changes: 1 addition & 1 deletion backend/Testing/Browser/Page/SandboxPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Testing.Browser.Page;

public class SandboxPage : BasePage<SandboxPage>
{
public SandboxPage(IPage page) : base(page, "/sandbox", page.Locator(":text('Sandbox')"))
public SandboxPage(IPage page) : base(page, "/sandbox", page.GetByRole(AriaRole.Heading, new() { Name = "Sandbox" }))
{
}
}
5 changes: 4 additions & 1 deletion backend/Testing/Browser/Page/TempUserDashboardPage.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.Playwright;
using Testing.Browser.Util;
using Testing.Services;

namespace Testing.Browser.Page;

Expand All @@ -14,6 +15,8 @@ public TempUserDashboardPage(IPage page, TempUser user) : base(page)

public async ValueTask DisposeAsync()
{
await Page.DeleteUser(User.Id);
var context = await Page.Context.Browser.NewContextAsync();
await context.APIRequest.LoginAs("admin", TestingEnvironmentVariables.DefaultPassword);
await context.APIRequest.DeleteUser(User.Id);
}
}
Loading