Disable chat deletion
This commit is contained in:
@@ -655,7 +655,7 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkDeleteQueuesMaxActionAndOutboxDeletesWithChatUrl()
|
||||
public async Task DeleteChatsIsDisabledAndKeepsChatVisible()
|
||||
{
|
||||
var bridge = new ResolvingDeleteMaxBridgeClient();
|
||||
using var factory = _factory.WithWebHostBuilder(builder =>
|
||||
@@ -670,12 +670,13 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
using var client = factory.CreateClient();
|
||||
await LoginAsync(client);
|
||||
|
||||
var chat = await CreateDirectChatAsync(client, "missing-url-chat", "Missing Url");
|
||||
var externalId = $"missing-url-chat-{Guid.NewGuid():N}";
|
||||
var chat = await CreateDirectChatAsync(client, externalId, "Missing Url");
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"/api/chats/delete",
|
||||
new ChatBulkActionRequest([chat.Id]),
|
||||
JsonOptions);
|
||||
await AssertStatusAsync(HttpStatusCode.NoContent, response);
|
||||
await AssertStatusAsync(HttpStatusCode.BadRequest, response);
|
||||
|
||||
Assert.Null(bridge.ResolvedExternalId);
|
||||
Assert.Empty(bridge.DeleteChatUrls);
|
||||
@@ -684,29 +685,17 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<QMaxDbContext>();
|
||||
var stored = await db.Chats.SingleAsync(x => x.Id == chat.Id);
|
||||
Assert.NotNull(stored.DeletedAt);
|
||||
Assert.Equal(ChatPendingMaxAction.DeleteChat, stored.PendingMaxAction);
|
||||
Assert.NotNull(stored.PendingMaxActionRequestedAt);
|
||||
Assert.Null(stored.DeletedAt);
|
||||
Assert.Null(stored.PendingMaxAction);
|
||||
Assert.Null(stored.PendingMaxActionRequestedAt);
|
||||
}
|
||||
|
||||
var chats = await client.GetFromJsonAsync<ChatDto[]>("/api/chats", JsonOptions);
|
||||
Assert.DoesNotContain(chats!, x => x.Id == chat.Id);
|
||||
|
||||
Assert.True(await ProcessOutboxAsync(factory) >= 1);
|
||||
Assert.Equal("https://max.test/chat/missing-url-chat", bridge.DeleteChatUrl);
|
||||
|
||||
await using (var scope = factory.Services.CreateAsyncScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<QMaxDbContext>();
|
||||
var stored = await db.Chats.SingleAsync(x => x.Id == chat.Id);
|
||||
Assert.Null(stored.PendingMaxAction);
|
||||
Assert.Equal("https://max.test/chat/missing-url-chat", stored.WebUrl);
|
||||
Assert.NotNull(stored.DeletedAt);
|
||||
}
|
||||
Assert.Contains(chats!, x => x.Id == chat.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkDeleteProcessesSeveralPendingChatActionsPerOutboxTick()
|
||||
public async Task DeleteChatsRejectsBulkRequestWithoutPendingActions()
|
||||
{
|
||||
var bridge = new ResolvingDeleteMaxBridgeClient();
|
||||
using var factory = _factory.WithWebHostBuilder(builder =>
|
||||
@@ -721,24 +710,31 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
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 suffix = Guid.NewGuid().ToString("N");
|
||||
var first = await CreateDirectChatAsync(client, $"bulk-delete-one-{suffix}", "Bulk Delete One");
|
||||
var second = await CreateDirectChatAsync(client, $"bulk-delete-two-{suffix}", "Bulk Delete Two");
|
||||
var third = await CreateDirectChatAsync(client, $"bulk-delete-three-{suffix}", "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);
|
||||
await AssertStatusAsync(HttpStatusCode.BadRequest, response);
|
||||
|
||||
Assert.True(await ProcessOutboxAsync(factory) >= 3);
|
||||
Assert.Equal(3, bridge.DeleteChatUrls.Count);
|
||||
Assert.Empty(bridge.DeleteChatUrls);
|
||||
|
||||
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);
|
||||
var storedChats = await db.Chats
|
||||
.Where(x => x.Id == first.Id || x.Id == second.Id || x.Id == third.Id)
|
||||
.ToListAsync();
|
||||
Assert.All(storedChats, stored =>
|
||||
{
|
||||
Assert.Null(stored.DeletedAt);
|
||||
Assert.Null(stored.PendingMaxAction);
|
||||
});
|
||||
|
||||
var chats = await client.GetFromJsonAsync<ChatDto[]>("/api/chats", JsonOptions);
|
||||
Assert.Equal(3, chats!.Count(x => x.Id == first.Id || x.Id == second.Id || x.Id == third.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1029,10 +1025,11 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkDeleteOutboxRefreshesStaleChatUrlAfterMenuOpenFailure()
|
||||
public async Task DeleteChatsDoesNotRefreshStaleChatUrlOrHideChat()
|
||||
{
|
||||
const string externalId = "stale-menu-url-chat";
|
||||
const string staleUrl = "https://max.test/chat/wrong-chat";
|
||||
var suffix = Guid.NewGuid().ToString("N");
|
||||
var externalId = $"stale-menu-url-chat-{suffix}";
|
||||
var staleUrl = $"https://max.test/chat/wrong-chat-{suffix}";
|
||||
var bridge = new ResolvingDeleteMaxBridgeClient(staleUrl);
|
||||
using var factory = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
@@ -1059,28 +1056,26 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
"/api/chats/delete",
|
||||
new ChatBulkActionRequest([chat.Id]),
|
||||
JsonOptions);
|
||||
await AssertStatusAsync(HttpStatusCode.NoContent, response);
|
||||
await AssertStatusAsync(HttpStatusCode.BadRequest, response);
|
||||
|
||||
Assert.Empty(bridge.DeleteChatUrls);
|
||||
|
||||
Assert.True(await ProcessOutboxAsync(factory) >= 1);
|
||||
Assert.Equal(externalId, bridge.ResolvedExternalId);
|
||||
Assert.Equal([staleUrl, $"https://max.test/chat/{externalId}"], bridge.DeleteChatUrls);
|
||||
Assert.Null(bridge.ResolvedExternalId);
|
||||
|
||||
var chats = await client.GetFromJsonAsync<ChatDto[]>("/api/chats", JsonOptions);
|
||||
Assert.DoesNotContain(chats!, x => x.Id == chat.Id);
|
||||
Assert.Contains(chats!, x => x.Id == chat.Id);
|
||||
|
||||
await using (var scope = factory.Services.CreateAsyncScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<QMaxDbContext>();
|
||||
var stored = await db.Chats.SingleAsync(x => x.Id == chat.Id);
|
||||
Assert.Null(stored.PendingMaxAction);
|
||||
Assert.Equal($"https://max.test/chat/{externalId}", stored.WebUrl);
|
||||
Assert.Null(stored.DeletedAt);
|
||||
Assert.Equal(staleUrl, stored.WebUrl);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OutboxSendsQueuedMessagesBeforePendingChatActions()
|
||||
public async Task OutboxSendsQueuedMessagesBeforePendingClearHistoryActions()
|
||||
{
|
||||
var bridge = new ResolvingDeleteMaxBridgeClient();
|
||||
using var factory = _factory.WithWebHostBuilder(builder =>
|
||||
@@ -1095,14 +1090,14 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
using var client = factory.CreateClient();
|
||||
await LoginAsync(client);
|
||||
|
||||
var deleteChat = await CreateDirectChatAsync(client, "delete-after-send-chat", "Delete After Send");
|
||||
var deleteResponse = await client.PostAsJsonAsync(
|
||||
"/api/chats/delete",
|
||||
new ChatBulkActionRequest([deleteChat.Id]),
|
||||
var clearChat = await CreateDirectChatAsync(client, "clear-after-send-chat", "Clear After Send");
|
||||
var clearResponse = await client.PostAsJsonAsync(
|
||||
"/api/chats/clear-history",
|
||||
new ChatBulkActionRequest([clearChat.Id]),
|
||||
JsonOptions);
|
||||
await AssertStatusAsync(HttpStatusCode.NoContent, deleteResponse);
|
||||
await AssertStatusAsync(HttpStatusCode.NoContent, clearResponse);
|
||||
|
||||
var sendChat = await CreateDirectChatAsync(client, "send-before-delete-chat", "Send Before Delete");
|
||||
var sendChat = await CreateDirectChatAsync(client, "send-before-clear-chat", "Send Before Clear");
|
||||
var messageResponse = await client.PostAsJsonAsync(
|
||||
$"/api/chats/{sendChat.Id}/messages",
|
||||
new SendMessageRequest("send before chat action"),
|
||||
@@ -1110,13 +1105,13 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
await AssertStatusAsync(HttpStatusCode.OK, messageResponse);
|
||||
|
||||
Assert.Equal(2, await ProcessOutboxAsync(factory));
|
||||
Assert.Equal(["send:text", "delete"], bridge.OperationOrder);
|
||||
Assert.Equal(["send:text", "clear"], bridge.OperationOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FailedPendingChatActionWaitsBeforeImmediateRetry()
|
||||
{
|
||||
var bridge = new ResolvingDeleteMaxBridgeClient { AlwaysFailDelete = true };
|
||||
var bridge = new ResolvingDeleteMaxBridgeClient { AlwaysFailClear = true };
|
||||
using var factory = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureTestServices(services =>
|
||||
@@ -1129,26 +1124,26 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
using var client = factory.CreateClient();
|
||||
await LoginAsync(client);
|
||||
|
||||
var chat = await CreateDirectChatAsync(client, "retry-delay-delete-chat", "Retry Delay Delete");
|
||||
var deleteResponse = await client.PostAsJsonAsync(
|
||||
"/api/chats/delete",
|
||||
var chat = await CreateDirectChatAsync(client, "retry-delay-clear-chat", "Retry Delay Clear");
|
||||
var clearResponse = await client.PostAsJsonAsync(
|
||||
"/api/chats/clear-history",
|
||||
new ChatBulkActionRequest([chat.Id]),
|
||||
JsonOptions);
|
||||
await AssertStatusAsync(HttpStatusCode.NoContent, deleteResponse);
|
||||
await AssertStatusAsync(HttpStatusCode.NoContent, clearResponse);
|
||||
|
||||
Assert.True(await ProcessOutboxAsync(factory) >= 1);
|
||||
Assert.Single(bridge.DeleteChatUrls);
|
||||
Assert.Single(bridge.ClearChatUrls);
|
||||
|
||||
await ProcessOutboxAsync(factory);
|
||||
Assert.Single(bridge.DeleteChatUrls);
|
||||
Assert.Single(bridge.ClearChatUrls);
|
||||
|
||||
await using var scope = factory.Services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<QMaxDbContext>();
|
||||
var stored = await db.Chats.SingleAsync(x => x.Id == chat.Id);
|
||||
Assert.Equal(ChatPendingMaxAction.DeleteChat, stored.PendingMaxAction);
|
||||
Assert.Equal(ChatPendingMaxAction.ClearHistory, stored.PendingMaxAction);
|
||||
Assert.Equal(1, stored.PendingMaxActionAttempts);
|
||||
Assert.NotNull(stored.PendingMaxActionLastAttemptAt);
|
||||
Assert.Equal("Simulated MAX delete failure.", stored.PendingMaxActionError);
|
||||
Assert.Equal("Simulated MAX clear failure.", stored.PendingMaxActionError);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1208,9 +1203,10 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SyncDoesNotRecreateLocallyDeletedChat()
|
||||
public async Task SyncKeepsChatVisibleAfterDisabledDeleteRequest()
|
||||
{
|
||||
const string externalId = "deleted-sync-chat";
|
||||
var suffix = Guid.NewGuid().ToString("N");
|
||||
var externalId = $"deleted-sync-chat-{suffix}";
|
||||
var sentAt = DateTimeOffset.UtcNow;
|
||||
var bridge = new ResolvingDeleteMaxBridgeClient
|
||||
{
|
||||
@@ -1223,7 +1219,7 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
sentAt,
|
||||
[
|
||||
new MaxMessageUpdate(
|
||||
"deleted-sync-message",
|
||||
$"deleted-sync-message-{suffix}",
|
||||
"remote-user",
|
||||
"Deleted Remote",
|
||||
false,
|
||||
@@ -1254,33 +1250,30 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
"/api/chats/delete",
|
||||
new ChatBulkActionRequest([chat.Id]),
|
||||
JsonOptions);
|
||||
await AssertStatusAsync(HttpStatusCode.NoContent, deleteResponse);
|
||||
await AssertStatusAsync(HttpStatusCode.BadRequest, 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.ExternalId == externalId);
|
||||
Assert.Contains(chats!, x => x.ExternalId == externalId);
|
||||
|
||||
await using var scope = factory.Services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<QMaxDbContext>();
|
||||
var stored = await db.Chats.Include(x => x.Messages).SingleAsync(x => x.Id == chat.Id);
|
||||
Assert.NotNull(stored.DeletedAt);
|
||||
Assert.Empty(stored.Messages);
|
||||
stored.PendingMaxAction = null;
|
||||
stored.PendingMaxActionRequestedAt = null;
|
||||
stored.PendingMaxActionLastAttemptAt = null;
|
||||
stored.PendingMaxActionError = null;
|
||||
await db.SaveChangesAsync();
|
||||
Assert.Null(stored.DeletedAt);
|
||||
Assert.Null(stored.PendingMaxAction);
|
||||
Assert.Contains(stored.Messages, x => x.ExternalId == $"deleted-sync-message-{suffix}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SyncDoesNotRecreateDeletedChatWhenExternalIdChangesButWebUrlMatches()
|
||||
public async Task SyncUpdatesVisibleChatWhenExternalIdChangesButWebUrlMatches()
|
||||
{
|
||||
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 suffix = Guid.NewGuid().ToString("N");
|
||||
var oldExternalId = $"deleted-sync-old-external-{suffix}";
|
||||
var newExternalId = $"deleted-sync-new-external-{suffix}";
|
||||
var title = $"Visible WebUrl Match {suffix}";
|
||||
var chatUrl = $"https://max.test/chat/visible-web-url-match-{suffix}";
|
||||
var sentAt = DateTimeOffset.UtcNow;
|
||||
var bridge = new ResolvingDeleteMaxBridgeClient
|
||||
{
|
||||
@@ -1324,25 +1317,21 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
"/api/chats/delete",
|
||||
new ChatBulkActionRequest([chat.Id]),
|
||||
JsonOptions);
|
||||
await AssertStatusAsync(HttpStatusCode.NoContent, deleteResponse);
|
||||
await AssertStatusAsync(HttpStatusCode.BadRequest, 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);
|
||||
Assert.Contains(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();
|
||||
var visibleChat = Assert.Single(storedChats);
|
||||
Assert.Equal(newExternalId, visibleChat.ExternalId);
|
||||
Assert.Null(visibleChat.DeletedAt);
|
||||
Assert.Null(visibleChat.PendingMaxAction);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user