Suppress pushes for outgoing preview echoes

This commit is contained in:
sevenhill
2026-07-04 22:46:18 +03:00
parent 293937f5d2
commit 0468d02bf3
2 changed files with 225 additions and 2 deletions
+167
View File
@@ -604,6 +604,68 @@ public sealed class ApiSmokeTests : IDisposable
Assert.Equal("hello ????", MessageTextSanitizer.CleanChatPreview("hello ????"));
}
[Fact]
public async Task SyncDoesNotPushListPreviewEchoOfOwnOutgoingMessage()
{
var pushNotifications = new RecordingPushNotificationService();
var bridge = new OutgoingPreviewEchoMaxBridgeClient(
$"mock-self-echo-chat-{Guid.NewGuid():N}",
"message from me");
using var factory = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.RemoveAll<IHostedService>();
services.RemoveAll<IMaxBridgeClient>();
services.RemoveAll<IPushNotificationService>();
services.AddSingleton<IMaxBridgeClient>(bridge);
services.AddSingleton<IPushNotificationService>(pushNotifications);
});
});
using var client = factory.CreateClient();
await LoginAsync(client);
Guid chatId;
var sentAt = DateTimeOffset.UtcNow.AddSeconds(-20);
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<QMaxDbContext>();
var chat = new Chat
{
ExternalId = bridge.ChatExternalId,
Title = "Self Echo",
LastMessagePreview = bridge.PreviewText,
LastMessageAt = sentAt,
UnreadCount = 0
};
db.Chats.Add(chat);
db.Messages.Add(new Message
{
Chat = chat,
ExternalId = "self-echo-message",
Direction = MessageDirection.Outgoing,
Text = bridge.PreviewText,
SentAt = sentAt,
DeliveryState = MessageDeliveryState.Sent
});
await db.SaveChangesAsync();
chatId = chat.Id;
}
var sync = await client.PostAsync("/api/max/sync", null);
await AssertStatusAsync(HttpStatusCode.OK, sync);
Assert.Empty(pushNotifications.SentMessages);
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<QMaxDbContext>();
var chat = await db.Chats.FindAsync(chatId);
Assert.NotNull(chat);
Assert.Equal(0, chat!.UnreadCount);
Assert.Equal(bridge.PreviewText, chat.LastMessagePreview);
}
}
[Fact]
public async Task FirebasePushSkipsOutgoingMessages()
{
@@ -749,6 +811,111 @@ public sealed class ApiSmokeTests : IDisposable
}
}
private sealed class RecordingPushNotificationService : IPushNotificationService
{
public List<(Chat Chat, Message Message)> SentMessages { get; } = [];
public Task SendNewMessageAsync(Chat chat, Message message, CancellationToken cancellationToken)
{
SentMessages.Add((chat, message));
return Task.CompletedTask;
}
}
private sealed class OutgoingPreviewEchoMaxBridgeClient : IMaxBridgeClient
{
public OutgoingPreviewEchoMaxBridgeClient(string chatExternalId, string previewText)
{
ChatExternalId = chatExternalId;
PreviewText = previewText;
}
public string ChatExternalId { get; }
public string PreviewText { get; }
public Task<MaxBridgeStatus> GetStatusAsync(CancellationToken cancellationToken)
{
return Task.FromResult(new MaxBridgeStatus("Test", true, "Authorized", "Ready", null, "Test", null, DateTimeOffset.UtcNow));
}
public Task<MaxBridgeStatus> BeginPhoneLoginAsync(string phoneNumber, CancellationToken cancellationToken)
{
return GetStatusAsync(cancellationToken);
}
public Task<MaxBridgeStatus> SubmitLoginCodeAsync(string code, CancellationToken cancellationToken)
{
return GetStatusAsync(cancellationToken);
}
public Task<MaxBrowserSnapshot> GetSnapshotAsync(CancellationToken cancellationToken)
{
return Task.FromResult(new MaxBrowserSnapshot(null, "Test", "", "", DateTimeOffset.UtcNow));
}
public Task<IReadOnlyList<MaxChatUpdate>> FetchUpdatesAsync(CancellationToken cancellationToken)
{
return Task.FromResult<IReadOnlyList<MaxChatUpdate>>(
[
new MaxChatUpdate(
ChatExternalId,
"Self Echo",
null,
DateTimeOffset.UtcNow,
[],
PreviewText,
null,
null,
DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromHours(3)).ToString("HH:mm"))
]);
}
public Task<MaxChatUpdate?> FetchChatHistoryAsync(string externalChatId, CancellationToken cancellationToken)
{
return Task.FromResult<MaxChatUpdate?>(null);
}
public Task<MaxChatPresence?> FetchChatPresenceAsync(string externalChatId, CancellationToken cancellationToken)
{
return Task.FromResult<MaxChatPresence?>(null);
}
public Task<MaxSendResult> SendTextAsync(string externalChatId, string text, CancellationToken cancellationToken)
{
return Task.FromResult(new MaxSendResult(true, $"external-{Guid.NewGuid():N}", null));
}
public Task<MaxSendResult> SendAttachmentAsync(string externalChatId, string path, string caption, CancellationToken cancellationToken)
{
return Task.FromResult(new MaxSendResult(true, $"external-file-{Guid.NewGuid():N}", null));
}
public Task<MaxActionResult> EditMessageAsync(string externalChatId, string externalMessageId, string? currentText, string text, CancellationToken cancellationToken)
{
return Task.FromResult(new MaxActionResult(true, null));
}
public Task<MaxActionResult> DeleteMessageAsync(string externalChatId, string externalMessageId, string? currentText, CancellationToken cancellationToken)
{
return Task.FromResult(new MaxActionResult(true, null));
}
public Task<MaxActionResult> SetReactionAsync(string externalChatId, string externalMessageId, string? currentText, string emoji, CancellationToken cancellationToken)
{
return Task.FromResult(new MaxActionResult(true, null));
}
public Task<MaxActionResult> ClearReactionAsync(string externalChatId, string externalMessageId, string? currentText, string emoji, CancellationToken cancellationToken)
{
return Task.FromResult(new MaxActionResult(true, null));
}
public Task<MaxMediaDownload?> DownloadMediaAsync(string remoteUrl, CancellationToken cancellationToken)
{
return Task.FromResult<MaxMediaDownload?>(null);
}
}
private sealed class FailingActionMaxBridgeClient : IMaxBridgeClient
{
public Task<MaxBridgeStatus> GetStatusAsync(CancellationToken cancellationToken)