From 912cd1bb70c7101a3b64d2a5ed2d277c656b320c Mon Sep 17 00:00:00 2001 From: Hamza Alqurneh Date: Thu, 24 Sep 2026 11:00:15 +0300 Subject: [PATCH 1/2] fix: make the request rate limits configurable Bitween:RateLimits overrides the sign-in and per-account limits; defaults stay 10 and 600. Lets the local profile run the e2e suite, which spends the per-account budget several times over. --- SW.Bitween.Web/Startup.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/SW.Bitween.Web/Startup.cs b/SW.Bitween.Web/Startup.cs index 5ac617f5..3f7f535a 100644 --- a/SW.Bitween.Web/Startup.cs +++ b/SW.Bitween.Web/Startup.cs @@ -585,9 +585,18 @@ private void RejectSampleSigningKey() /// a replacement for the per-account lockout, which counts attempts against one account /// across every address; this counts them per address across every account. /// + /// + /// Both numbers can be overridden through Bitween:RateLimits, and are unchanged wherever + /// that is not set. The end-to-end suite needs it: it drives the UI far faster than a person, + /// all as one account, and spends the per-account budget several times over in a run. + /// /// - private static void AddRateLimiting(IServiceCollection services) + private void AddRateLimiting(IServiceCollection services) { + var limits = Configuration.GetSection("Bitween:RateLimits"); + var signInLimit = limits.GetValue("SignInPerMinute", 10); + var requestLimit = limits.GetValue("RequestsPerMinute", 600); + services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; @@ -599,7 +608,7 @@ private static void AddRateLimiting(IServiceCollection services) $"signin:{ClientAddress(context)}", _ => new FixedWindowRateLimiterOptions { - PermitLimit = 10, + PermitLimit = signInLimit, Window = TimeSpan.FromMinutes(1) }); @@ -609,7 +618,7 @@ private static void AddRateLimiting(IServiceCollection services) account is null ? $"anon:{ClientAddress(context)}" : $"account:{account}", _ => new FixedWindowRateLimiterOptions { - PermitLimit = 600, + PermitLimit = requestLimit, Window = TimeSpan.FromMinutes(1) }); }); From ddb5b274d26a339ab38641db3a4b97957c6fe7b9 Mon Sep 17 00:00:00 2001 From: Hamza Alqurneh Date: Thu, 24 Sep 2026 11:00:15 +0300 Subject: [PATCH 2/2] test: stop two e2e specs racing or leaning on local data --- SW.Bitween.Web/ClientApp/e2e/readable-documents.spec.ts | 3 +++ SW.Bitween.Web/ClientApp/e2e/table-layout.spec.ts | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/SW.Bitween.Web/ClientApp/e2e/readable-documents.spec.ts b/SW.Bitween.Web/ClientApp/e2e/readable-documents.spec.ts index 778df23b..340bf0e4 100644 --- a/SW.Bitween.Web/ClientApp/e2e/readable-documents.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/readable-documents.spec.ts @@ -179,6 +179,9 @@ test("Raw shows the bytes as they arrived, uncoloured", async ({ page }) => { const row = page.getByRole("row").nth(1); await expect(row).toBeVisible({ timeout: 15000 }); await row.locator("td").last().click(); + // The drawer opens on the furthest stage with a document, and whether mapping has + // finished by now is a race. Raw is a promise about what arrived, so ask for that. + await page.getByTitle("Show the Input document").click(); // Formatted is the default, and it parsed, so it is coloured. const pane = page.locator(".doc-hl-dark"); diff --git a/SW.Bitween.Web/ClientApp/e2e/table-layout.spec.ts b/SW.Bitween.Web/ClientApp/e2e/table-layout.spec.ts index 872d5e33..c7bcfcff 100644 --- a/SW.Bitween.Web/ClientApp/e2e/table-layout.spec.ts +++ b/SW.Bitween.Web/ClientApp/e2e/table-layout.spec.ts @@ -206,7 +206,12 @@ test("a panel list pages and filters once it runs long", async ({ page }) => { }); await page.goto("information-types"); - await page.locator("tbody tr").first().click(); + // The route above can only multiply a subscription that exists, so open a type something + // uses — the first row is just whichever type was made last. + const usedBy = page + .locator('a[href*="/subscriptions/"]') + .or(page.getByRole("button", { name: /^Show all \d+ subscriptions$/ })); + await page.locator("tbody tr").filter({ has: usedBy }).first().locator("td").nth(1).click(); await expect(page).toHaveURL(/\/information-types\/\d+$/); // Long names in a ~360px panel used to push Type off the right-hand edge.