Mill votes: the very-busy refusal offers sign-in to a voter who is not signed in
Accounts have their own table, so signing in works while the cast or mint table is full. A signed-in voter is not told to sign in. Pinned through the full pipeline at a small cap, for both audiences. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a9b9b29bf9
commit
cf74019194
2 changed files with 35 additions and 6 deletions
|
|
@ -82,7 +82,7 @@ public sealed class VoteModel(
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Throttled(limits.DenyCast(HttpContext.Connection.RemoteIpAddress, voter), CastRefusal) is { } throttled)
|
if (Throttled(limits.DenyCast(HttpContext.Connection.RemoteIpAddress, voter), CastRefusal, voter) is { } throttled)
|
||||||
{
|
{
|
||||||
return throttled;
|
return throttled;
|
||||||
}
|
}
|
||||||
|
|
@ -105,14 +105,14 @@ public sealed class VoteModel(
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Throttled(limits.DenyCast(client, voter), CastRefusal) is { } throttled)
|
if (Throttled(limits.DenyCast(client, voter), CastRefusal, voter) is { } throttled)
|
||||||
{
|
{
|
||||||
return throttled;
|
return throttled;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (voter is null)
|
if (voter is null)
|
||||||
{
|
{
|
||||||
if (Throttled(limits.DenyMint(client), MintRefusal) is { } mintThrottled)
|
if (Throttled(limits.DenyMint(client), MintRefusal, voter) is { } mintThrottled)
|
||||||
{
|
{
|
||||||
return mintThrottled;
|
return mintThrottled;
|
||||||
}
|
}
|
||||||
|
|
@ -143,14 +143,19 @@ public sealed class VoteModel(
|
||||||
|
|
||||||
private const string BusyRefusal = "Voting is very busy right now. Please try again in a little while.";
|
private const string BusyRefusal = "Voting is very busy right now. Please try again in a little while.";
|
||||||
|
|
||||||
private IActionResult? Throttled(BudgetDenial? denial, string spentRefusal)
|
// Accounts have their own table, so a visitor who is not signed in can still vote by signing in while a table is full.
|
||||||
|
private const string BusyRefusalSignIn =
|
||||||
|
"Voting is very busy right now. You can sign in to vote now, or try again in a little while.";
|
||||||
|
|
||||||
|
private IActionResult? Throttled(BudgetDenial? denial, string spentRefusal, VoterIdentity? voter)
|
||||||
{
|
{
|
||||||
if (denial is not { } refused)
|
if (denial is not { } refused)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var refusal = refused.TableFull ? BusyRefusal : spentRefusal;
|
var busy = voter?.UserAccountId is null ? BusyRefusalSignIn : BusyRefusal;
|
||||||
|
var refusal = refused.TableFull ? busy : spentRefusal;
|
||||||
Response.Headers.RetryAfter = ((int)Math.Ceiling(refused.RetryAfter.TotalSeconds)).ToString(System.Globalization.CultureInfo.InvariantCulture);
|
Response.Headers.RetryAfter = ((int)Math.Ceiling(refused.RetryAfter.TotalSeconds)).ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
return new ContentResult
|
return new ContentResult
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -381,12 +381,36 @@ public sealed class ConceptVoteRateLimitTests : IClassFixture<ConceptVoteRateLim
|
||||||
var body = await visitor.Content.ReadAsStringAsync();
|
var body = await visitor.Content.ReadAsStringAsync();
|
||||||
Assert.Contains("busy", body);
|
Assert.Contains("busy", body);
|
||||||
Assert.DoesNotContain("too fast", body);
|
Assert.DoesNotContain("too fast", body);
|
||||||
Assert.DoesNotContain("sign in", body, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("sign in", body, StringComparison.OrdinalIgnoreCase);
|
||||||
Assert.DoesNotContain("203.0.113", body + string.Join(";", visitor.Headers.Select(h => string.Join(",", h.Value))));
|
Assert.DoesNotContain("203.0.113", body + string.Join(";", visitor.Headers.Select(h => string.Join(",", h.Value))));
|
||||||
Assert.Equal(HttpStatusCode.Redirect, account.StatusCode);
|
Assert.Equal(HttpStatusCode.Redirect, account.StatusCode);
|
||||||
Assert.Equal(rowsBefore + 1, (await Rows(collection)).Count);
|
Assert.Equal(rowsBefore + 1, (await Rows(collection)).Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task A_full_account_table_refuses_a_new_account_as_busy_without_offering_sign_in()
|
||||||
|
{
|
||||||
|
var factory = Capped(4);
|
||||||
|
var collection = await NewSubject();
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
var spender = new Client(factory, $"203.0.113.{150 + i}") { Account = ($"72{i:00}", $"spent-account-{i}") };
|
||||||
|
for (var cast = 0; cast < 3; cast++)
|
||||||
|
{
|
||||||
|
using var ok = await spender.Post(collection, "Up");
|
||||||
|
Assert.Equal(HttpStatusCode.Redirect, ok.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using var fresh = await new Client(factory, "203.0.113.160") { Account = ("7299", "fresh-signed-in") }
|
||||||
|
.Post(collection, "Up");
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.TooManyRequests, fresh.StatusCode);
|
||||||
|
var body = await fresh.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains("busy", body);
|
||||||
|
Assert.DoesNotContain("sign in", body, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Posts_to_a_missing_subject_spend_no_budget_and_mint_no_device()
|
public async Task Posts_to_a_missing_subject_spend_no_budget_and_mint_no_device()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue