using System.Net; using System.Net.Http.Json; using YesChef.Api.Auth; using YesChef.Api.IntegrationTests.Infrastructure; namespace YesChef.Api.IntegrationTests.Features; /// /// Verifies the auth-endpoint rate limits actually fire. Uses a /// non-"Testing" environment so the limiter is not bypassed; that means a /// fresh app/factory is constructed per test rather than reusing the base /// class's Testing-env app. /// public class AuthRateLimitTests : IntegrationTest { [Test] public async Task Login_returns_429_after_exhausting_limit() { await using var liveApp = new YesChefAppFactory { ConnectionString = await CloneDatabaseAsync(), EnvironmentName = "Production", }; using var client = liveApp.CreateClient(); // Limit is 10 per 15-minute window. Eleventh call must be rejected. // Using a non-existent user keeps each request a 401 — wrong-password // path still counts toward the limit. HttpResponseMessage? lastResponse = null; for (var i = 0; i < 11; i++) { lastResponse?.Dispose(); lastResponse = await client.PostAsJsonAsync("/api/auth/login", new AuthEndpoints.LoginRequest("ghost", "anything")); } await Assert.That(lastResponse!.StatusCode).IsEqualTo(HttpStatusCode.TooManyRequests); await Assert.That(lastResponse.Headers.Contains("Retry-After")).IsTrue(); lastResponse.Dispose(); } [Test] public async Task Register_returns_429_after_exhausting_limit() { await using var liveApp = new YesChefAppFactory { ConnectionString = await CloneDatabaseAsync(), EnvironmentName = "Production", }; using var client = liveApp.CreateClient(); // Limit is 5 per hour. Sixth call must be rejected. Use unique names so // we don't trip the duplicate-name check before the limiter does. HttpResponseMessage? lastResponse = null; for (var i = 0; i < 6; i++) { lastResponse?.Dispose(); lastResponse = await client.PostAsJsonAsync("/api/auth/register", new AuthEndpoints.RegisterRequest($"rate-{i}", "pw-1234", YesChefAppFactory.FamilyCode)); } await Assert.That(lastResponse!.StatusCode).IsEqualTo(HttpStatusCode.TooManyRequests); lastResponse.Dispose(); } [Test] public async Task Testing_env_bypasses_limit() { // The base class's app uses the Testing env; firing well past the // production limit must keep returning 401 (not 429). Sanity check // that the existing AuthEndpointsTests aren't going to start flaking. for (var i = 0; i < 15; i++) { using var response = await AnonymousClient.PostAsJsonAsync("/api/auth/login", new AuthEndpoints.LoginRequest("ghost", "anything")); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Unauthorized); } } /// /// The IntegrationTest base allocates one DB per test. We need a second /// independent connection string for the live-env factory; clone again /// from the same Postgres fixture. /// private async Task CloneDatabaseAsync() { var db = await Postgres.CreateDatabaseAsync(); // Leak the TestDatabase wrapper — the connection string lives as long // as the Postgres fixture (per-session). Acceptable for a tiny number // of rate-limit tests. return db.ConnectionString; } }