Skip to content
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
52 changes: 43 additions & 9 deletions API.IntegrationTests/Helpers/MailpitHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public MailpitHelper(string apiBaseUrl)
/// Polls until at least one email arrives for the given recipient address, or the timeout elapses.
/// Returns null if no message arrived within the timeout.
/// </summary>
/// <remarks>
/// Uses Mailpit's server-side search so the lookup is unaffected by how many unrelated messages
/// have accumulated in the inbox. Listing endpoints page at 50 by default which silently hid
/// matches once enough emails piled up across a test session.
/// </remarks>
public async Task<MailpitMessage?> WaitForMessageAsync(
string toAddress,
TimeSpan? timeout = null,
Expand All @@ -27,20 +32,49 @@ public MailpitHelper(string apiBaseUrl)
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(15));
while (DateTime.UtcNow < deadline && !cancellationToken.IsCancellationRequested)
{
var response = await _client.GetFromJsonAsync<MailpitSearchResponse>(
"/api/v1/messages?limit=50", cancellationToken);

var match = response?.Messages?.FirstOrDefault(m =>
m.To?.Any(c => c.Address.Equals(toAddress, StringComparison.OrdinalIgnoreCase)) == true);

if (match is not null)
return match;

var match = (await SearchByRecipientAsync(toAddress, limit: 1, cancellationToken)).FirstOrDefault();
if (match is not null) return match;
await Task.Delay(300, cancellationToken);
}
return null;
}

/// <summary>
/// Polls until at least <paramref name="minCount"/> emails are present for the recipient, or
/// the timeout elapses. Useful when a test expects multiple emails to arrive (e.g. two reset
/// requests in a row) and needs to disambiguate them.
/// </summary>
public async Task<List<MailpitMessage>> WaitForMessagesAsync(
string toAddress,
int minCount,
TimeSpan? timeout = null,
CancellationToken cancellationToken = default)
{
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(15));
while (DateTime.UtcNow < deadline && !cancellationToken.IsCancellationRequested)
{
var matches = await SearchByRecipientAsync(toAddress, limit: Math.Max(minCount, 10), cancellationToken);
if (matches.Count >= minCount) return matches;
await Task.Delay(300, cancellationToken);
}
return await SearchByRecipientAsync(toAddress, limit: Math.Max(minCount, 10), cancellationToken);
}

/// <summary>
/// Returns all messages currently in Mailpit addressed to the given recipient.
/// Server-side filtered via the Mailpit search API.
/// </summary>
public async Task<List<MailpitMessage>> SearchByRecipientAsync(
string toAddress,
int limit = 10,
CancellationToken cancellationToken = default)
{
var query = Uri.EscapeDataString($"to:{toAddress}");
var response = await _client.GetFromJsonAsync<MailpitSearchResponse>(
$"/api/v1/search?query={query}&limit={limit}", cancellationToken);
return response?.Messages ?? [];
}

/// <summary>
/// Returns all messages in Mailpit (no filtering).
/// </summary>
Expand Down
20 changes: 20 additions & 0 deletions API.IntegrationTests/Helpers/TestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,26 @@ public static StringContent JsonContent(object obj)
{
return new StringContent(JsonSerializer.Serialize(obj, JsonOptions), Encoding.UTF8, "application/json");
}

/// <summary>
/// Generates a unique recipient address so per-test Mailpit lookups never collide with other
/// tests' emails. Format: <c>{prefix}-{8hex}@test.org</c>.
/// </summary>
public static string UniqueEmail(string prefix)
{
var suffix = Guid.CreateVersion7().ToString("N")[..8];
return $"{prefix}-{suffix}@test.org";
}

/// <summary>
/// Generates a unique username so concurrent tests in the same session never collide on the
/// users.name unique index.
/// </summary>
public static string UniqueUsername(string prefix)
{
var suffix = Guid.CreateVersion7().ToString("N")[..8];
return ($"{prefix}{suffix}").ToLowerInvariant();
}
}

public sealed record AuthenticatedUser(Guid Id, string Username, string Email, string SessionToken);
Loading
Loading