fix: align backend APIs and upload flow
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
using MessageService.Domain.Entities;
|
||||
using MessageService.Domain.Enums;
|
||||
using MessageService.Infrastructure;
|
||||
using MessageService.Infrastructure.Reposities;
|
||||
using MessageService.Domain.KeyObjects;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using MySqlConnector;
|
||||
using Testcontainers.MySql;
|
||||
using Xunit;
|
||||
|
||||
namespace MessageService.Tests;
|
||||
|
||||
public sealed class ConversationUniquenessTests : IAsyncLifetime
|
||||
{
|
||||
private readonly MySqlContainer mysql = new MySqlBuilder("mysql:8.0").Build();
|
||||
|
||||
public Task InitializeAsync() =>
|
||||
string.Equals(Environment.GetEnvironmentVariable("RUN_DOCKER_TESTS"), "1", StringComparison.Ordinal)
|
||||
? mysql.StartAsync()
|
||||
: Task.CompletedTask;
|
||||
|
||||
public Task DisposeAsync() => mysql.DisposeAsync().AsTask();
|
||||
|
||||
[DockerFact]
|
||||
[Trait("Category", "Docker")]
|
||||
public async Task Migration_deduplicates_active_rows_and_enforces_active_uniqueness()
|
||||
{
|
||||
await using var db = CreateContext();
|
||||
var migrator = db.Database.GetService<IMigrator>();
|
||||
await migrator.MigrateAsync("20260909000100_ApiAlignmentFixes");
|
||||
|
||||
var userId = Guid.NewGuid();
|
||||
var targetId = Guid.NewGuid();
|
||||
await InsertConversationAsync(db, Guid.NewGuid(), userId, targetId, 2, 10, new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
await InsertConversationAsync(db, Guid.NewGuid(), userId, targetId, 7, 15, new DateTimeOffset(2026, 2, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
|
||||
await migrator.MigrateAsync();
|
||||
|
||||
var all = await db.Conversations.IgnoreQueryFilters().AsNoTracking().ToListAsync();
|
||||
var active = all.Single(x => !x.IsDeleted);
|
||||
Assert.Equal(2, all.Count);
|
||||
Assert.Equal(7, active.UnreadCount);
|
||||
Assert.Equal(15, active.LastReadSequenceId);
|
||||
|
||||
await Assert.ThrowsAsync<MySqlException>(() =>
|
||||
InsertConversationAsync(db, Guid.NewGuid(), userId, targetId, 0, null, DateTimeOffset.UtcNow));
|
||||
|
||||
db.ChangeTracker.Clear();
|
||||
var trackedActive = await db.Conversations.SingleAsync();
|
||||
trackedActive.SoftDelete();
|
||||
await db.SaveChangesAsync();
|
||||
await InsertConversationAsync(db, Guid.NewGuid(), userId, targetId, 0, null, DateTimeOffset.UtcNow);
|
||||
Assert.Equal(1, await db.Conversations.CountAsync());
|
||||
}
|
||||
|
||||
[DockerFact]
|
||||
[Trait("Category", "Docker")]
|
||||
public async Task Repository_uses_exact_active_lookup_and_orders_owner_list_newest_first()
|
||||
{
|
||||
await using var db = CreateContext();
|
||||
await db.Database.MigrateAsync();
|
||||
var userId = Guid.NewGuid();
|
||||
var firstTarget = Guid.NewGuid();
|
||||
var secondTarget = Guid.NewGuid();
|
||||
await InsertConversationAsync(db, Guid.NewGuid(), userId, firstTarget, 0, null, new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
await InsertConversationAsync(db, Guid.NewGuid(), userId, secondTarget, 0, null, new DateTimeOffset(2026, 2, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
var repository = new ConversationReposity(db);
|
||||
|
||||
var exact = await repository.FindActiveAsync(userId, firstTarget, ChatType.PRIVATE);
|
||||
var list = (await repository.ListByUserIdAsync(userId)).ToList();
|
||||
|
||||
Assert.NotNull(exact);
|
||||
Assert.Equal(firstTarget, exact.TargetId);
|
||||
Assert.Equal(secondTarget, list[0].TargetId);
|
||||
}
|
||||
|
||||
[DockerFact]
|
||||
[Trait("Category", "Docker")]
|
||||
public async Task Message_search_is_scoped_filtered_and_uses_an_exclusive_cursor()
|
||||
{
|
||||
await using var db = CreateContext();
|
||||
await db.Database.MigrateAsync();
|
||||
var senderId = Guid.NewGuid();
|
||||
var targetId = Guid.NewGuid();
|
||||
var context = new MessageCreateContext(ChatType.PRIVATE, Guid.NewGuid(), senderId, targetId);
|
||||
var first = Message.BuildTxt(context, "项目进度 一", 1);
|
||||
var second = Message.BuildTxt(context with { ClientMsgId = Guid.NewGuid() }, "项目进度 二", 2);
|
||||
var withdrawn = Message.BuildTxt(context with { ClientMsgId = Guid.NewGuid() }, "项目进度 已撤回", 3);
|
||||
withdrawn.Withdraw();
|
||||
var image = Message.BuildImg(context with { ClientMsgId = Guid.NewGuid() }, "image", 10, 10, "thumb", 4);
|
||||
var other = Message.BuildTxt(
|
||||
new MessageCreateContext(ChatType.GROUP, Guid.NewGuid(), senderId, Guid.NewGuid()),
|
||||
"项目进度 其他会话",
|
||||
5);
|
||||
db.Messages.AddRange(first, second, withdrawn, image, other);
|
||||
await db.SaveChangesAsync();
|
||||
var repository = new MessageReposity(db);
|
||||
|
||||
var firstPage = await repository.SearchAsync(first.StreamKey, "项目进度", null, 1);
|
||||
var secondPage = await repository.SearchAsync(first.StreamKey, "项目进度", firstPage.messages.Single().SequenceId, 10);
|
||||
|
||||
Assert.True(firstPage.hasMore);
|
||||
Assert.Equal(2, firstPage.messages.Single().SequenceId);
|
||||
Assert.False(secondPage.hasMore);
|
||||
Assert.Equal(1, secondPage.messages.Single().SequenceId);
|
||||
}
|
||||
|
||||
private MessageDbContext CreateContext()
|
||||
{
|
||||
var connectionString = mysql.GetConnectionString();
|
||||
var options = new DbContextOptionsBuilder<MessageDbContext>()
|
||||
.UseMySql(
|
||||
connectionString,
|
||||
new MySqlServerVersion(new Version(8, 0)),
|
||||
mysqlOptions => mysqlOptions.EnableRetryOnFailure(3, TimeSpan.FromSeconds(2), null))
|
||||
.Options;
|
||||
return new MessageDbContext(options, null!);
|
||||
}
|
||||
|
||||
private static Task InsertConversationAsync(
|
||||
MessageDbContext db,
|
||||
Guid id,
|
||||
Guid userId,
|
||||
Guid targetId,
|
||||
int unreadCount,
|
||||
long? lastReadSequenceId,
|
||||
DateTimeOffset modificationTime)
|
||||
{
|
||||
return db.Database.ExecuteSqlInterpolatedAsync($"""
|
||||
INSERT INTO `conversations`
|
||||
(`Id`, `UserId`, `TargetId`, `TargetAvatar`, `TargetName`, `LastReadSequenceId`,
|
||||
`UnreadCount`, `ChatType`, `StreamKey`, `LastMessage`, `CreationTime`,
|
||||
`ModificationTime`, `IsDeleted`, `Deletion`)
|
||||
VALUES
|
||||
({id}, {userId}, {targetId}, {string.Empty}, {"target"}, {lastReadSequenceId},
|
||||
{unreadCount}, {(int)ChatType.PRIVATE}, {"private-stream"}, {string.Empty}, {modificationTime},
|
||||
{modificationTime}, {false}, {null});
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DockerFactAttribute : FactAttribute
|
||||
{
|
||||
public DockerFactAttribute()
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("RUN_DOCKER_TESTS"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Skip = "Set RUN_DOCKER_TESTS=1 when a Docker daemon is available.";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user