Fix async chat actions and channel sync
This commit is contained in:
@@ -705,6 +705,42 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkDeleteProcessesSeveralPendingChatActionsPerOutboxTick()
|
||||
{
|
||||
var bridge = new ResolvingDeleteMaxBridgeClient();
|
||||
using var factory = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.RemoveAll<IHostedService>();
|
||||
services.RemoveAll<IMaxBridgeClient>();
|
||||
services.AddSingleton<IMaxBridgeClient>(bridge);
|
||||
});
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
await LoginAsync(client);
|
||||
|
||||
var first = await CreateDirectChatAsync(client, "bulk-delete-one", "Bulk Delete One");
|
||||
var second = await CreateDirectChatAsync(client, "bulk-delete-two", "Bulk Delete Two");
|
||||
var third = await CreateDirectChatAsync(client, "bulk-delete-three", "Bulk Delete Three");
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"/api/chats/delete",
|
||||
new ChatBulkActionRequest([first.Id, second.Id, third.Id]),
|
||||
JsonOptions);
|
||||
await AssertStatusAsync(HttpStatusCode.NoContent, response);
|
||||
|
||||
Assert.True(await ProcessOutboxAsync(factory) >= 3);
|
||||
Assert.Equal(3, bridge.DeleteChatUrls.Count);
|
||||
|
||||
await using var scope = factory.Services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<QMaxDbContext>();
|
||||
var pendingCount = await db.Chats.CountAsync(x =>
|
||||
(x.Id == first.Id || x.Id == second.Id || x.Id == third.Id) &&
|
||||
x.PendingMaxAction != null);
|
||||
Assert.Equal(0, pendingCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMessagesReturnsSqlCacheWithoutInlineMaxHistorySyncByDefault()
|
||||
{
|
||||
@@ -870,6 +906,66 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SyncCreatesNewChatAndPreviewMessageForFreshIncomingListUpdate()
|
||||
{
|
||||
var pushNotifications = new RecordingPushNotificationService();
|
||||
var marker = Guid.NewGuid().ToString("N");
|
||||
var externalId = $"fresh-preview-chat-{marker}";
|
||||
var title = "Fresh Preview Chat";
|
||||
var previewText = $"fresh incoming {marker}";
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var bridge = new ResolvingDeleteMaxBridgeClient
|
||||
{
|
||||
Updates =
|
||||
[
|
||||
new MaxChatUpdate(
|
||||
externalId,
|
||||
title,
|
||||
null,
|
||||
now,
|
||||
[],
|
||||
previewText,
|
||||
false,
|
||||
now,
|
||||
now.ToOffset(TimeSpan.FromHours(3)).ToString("HH:mm"),
|
||||
$"https://max.test/chat/{externalId}")
|
||||
]
|
||||
};
|
||||
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);
|
||||
|
||||
var sync = await client.PostAsync("/api/max/sync", null);
|
||||
await AssertStatusAsync(HttpStatusCode.OK, sync);
|
||||
|
||||
await using var scope = factory.Services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<QMaxDbContext>();
|
||||
var chat = await db.Chats.SingleAsync(x => x.ExternalId == externalId);
|
||||
Assert.Equal(title, chat.Title);
|
||||
Assert.Equal(previewText, chat.LastMessagePreview);
|
||||
Assert.Equal(1, chat.UnreadCount);
|
||||
|
||||
var message = await db.Messages.SingleAsync(x => x.ChatId == chat.Id);
|
||||
Assert.Equal(previewText, message.Text);
|
||||
Assert.Equal(MessageDirection.Incoming, message.Direction);
|
||||
Assert.StartsWith("preview:", message.ExternalId);
|
||||
|
||||
var previewPush = Assert.Single(pushNotifications.SentMessages);
|
||||
Assert.Equal(chat.Id, previewPush.Chat.Id);
|
||||
Assert.Equal(previewText, previewPush.Message.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SyncDoesNotCreateTextOnlyMessageForGenericMediaPreviewOnlyIncomingUpdate()
|
||||
{
|
||||
@@ -929,10 +1025,7 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
x.ChatId == chat.Id &&
|
||||
x.ExternalId != null &&
|
||||
x.ExternalId.StartsWith("preview:")));
|
||||
var previewPush = Assert.Single(pushNotifications.SentMessages);
|
||||
Assert.Equal(chat.Id, previewPush.Chat.Id);
|
||||
Assert.Equal("\u041c\u0435\u0434\u0438\u0430", previewPush.Message.Text);
|
||||
Assert.Equal(MessageDirection.Incoming, previewPush.Message.Direction);
|
||||
Assert.Empty(pushNotifications.SentMessages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1181,6 +1274,77 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SyncDoesNotRecreateDeletedChatWhenExternalIdChangesButWebUrlMatches()
|
||||
{
|
||||
const string oldExternalId = "deleted-sync-old-external";
|
||||
const string newExternalId = "deleted-sync-new-external";
|
||||
const string title = "Deleted Tombstone";
|
||||
const string chatUrl = "https://max.test/chat/deleted-tombstone";
|
||||
var sentAt = DateTimeOffset.UtcNow;
|
||||
var bridge = new ResolvingDeleteMaxBridgeClient
|
||||
{
|
||||
Updates =
|
||||
[
|
||||
new MaxChatUpdate(
|
||||
newExternalId,
|
||||
title,
|
||||
null,
|
||||
sentAt,
|
||||
[],
|
||||
"remote hello after external id change",
|
||||
false,
|
||||
sentAt,
|
||||
sentAt.ToOffset(TimeSpan.FromHours(3)).ToString("HH:mm"),
|
||||
chatUrl)
|
||||
]
|
||||
};
|
||||
using var factory = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.RemoveAll<IHostedService>();
|
||||
services.RemoveAll<IMaxBridgeClient>();
|
||||
services.AddSingleton<IMaxBridgeClient>(bridge);
|
||||
});
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
await LoginAsync(client);
|
||||
|
||||
var chat = await CreateDirectChatAsync(client, oldExternalId, title);
|
||||
await using (var scope = factory.Services.CreateAsyncScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<QMaxDbContext>();
|
||||
var stored = await db.Chats.SingleAsync(x => x.Id == chat.Id);
|
||||
stored.WebUrl = chatUrl;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var deleteResponse = await client.PostAsJsonAsync(
|
||||
"/api/chats/delete",
|
||||
new ChatBulkActionRequest([chat.Id]),
|
||||
JsonOptions);
|
||||
await AssertStatusAsync(HttpStatusCode.NoContent, deleteResponse);
|
||||
|
||||
var sync = await client.PostAsync("/api/max/sync", null);
|
||||
await AssertStatusAsync(HttpStatusCode.OK, sync);
|
||||
|
||||
var chats = await client.GetFromJsonAsync<ChatDto[]>("/api/chats", JsonOptions);
|
||||
Assert.DoesNotContain(chats!, x => x.Title == title);
|
||||
|
||||
await using var verifyScope = factory.Services.CreateAsyncScope();
|
||||
var verifyDb = verifyScope.ServiceProvider.GetRequiredService<QMaxDbContext>();
|
||||
var storedChats = await verifyDb.Chats.Where(x => x.Title == title).ToListAsync();
|
||||
var tombstone = Assert.Single(storedChats);
|
||||
Assert.Equal(oldExternalId, tombstone.ExternalId);
|
||||
Assert.NotNull(tombstone.DeletedAt);
|
||||
tombstone.PendingMaxAction = null;
|
||||
tombstone.PendingMaxActionRequestedAt = null;
|
||||
tombstone.PendingMaxActionLastAttemptAt = null;
|
||||
tombstone.PendingMaxActionError = null;
|
||||
await verifyDb.SaveChangesAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FailedMaxTextSendMarksLocalMessageFailed()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user