feat(friend): implement 5 read methods on FriendService + register DI + read test suite
GetFriendsAsync, GetReceiveAppliesAsync, GetSendAppliesAsync, GetPlayedTogetherAsync, SearchAsync all implemented. LoadViewerProjectionAsync materialises the full Viewer entity (with Include/ThenInclude for SelectedEmblem/Degree) then projects in-memory — avoids the EF Core limitation where Include is silently ignored under Select. FriendService + IPlayedTogetherWriter registered as Scoped in Program.cs. 12 read tests, all green; full suite 1171/1171 still passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,20 +28,119 @@ public sealed class FriendService : IFriendService, IPlayedTogetherWriter
|
|||||||
_log = log;
|
_log = log;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<FriendInfoResult> GetFriendsAsync(long viewerId, CancellationToken ct) =>
|
public async Task<FriendInfoResult> GetFriendsAsync(long viewerId, CancellationToken ct)
|
||||||
throw new NotImplementedException();
|
{
|
||||||
|
var friendIds = await _db.ViewerFriends
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(f => f.OwnerViewerId == viewerId)
|
||||||
|
.OrderBy(f => f.CreatedAt).ThenBy(f => f.FriendViewerId)
|
||||||
|
.Select(f => f.FriendViewerId)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
public Task<ReceiveApplyInfoResult> GetReceiveAppliesAsync(long viewerId, CancellationToken ct) =>
|
var friends = new List<FriendEntry>(friendIds.Count);
|
||||||
throw new NotImplementedException();
|
foreach (var friendId in friendIds)
|
||||||
|
{
|
||||||
|
var entry = await BuildFriendEntryAsync(friendId, ct);
|
||||||
|
if (entry is not null) friends.Add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
public Task<SendApplyInfoResult> GetSendAppliesAsync(long viewerId, CancellationToken ct) =>
|
return new FriendInfoResult(friends, friends.Count, FriendMaxCount);
|
||||||
throw new NotImplementedException();
|
}
|
||||||
|
|
||||||
public Task<PlayedTogetherResult> GetPlayedTogetherAsync(long viewerId, CancellationToken ct) =>
|
public async Task<ReceiveApplyInfoResult> GetReceiveAppliesAsync(long viewerId, CancellationToken ct)
|
||||||
throw new NotImplementedException();
|
{
|
||||||
|
var rows = await _db.ViewerFriendApplies
|
||||||
|
.Where(a => a.ToViewerId == viewerId)
|
||||||
|
.OrderBy(a => a.CreatedAt).ThenBy(a => a.Id)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
public Task<FriendEntry?> SearchAsync(long viewerId, int targetViewerId, CancellationToken ct) =>
|
var applies = new List<FriendApplyEntry>(rows.Count);
|
||||||
throw new NotImplementedException();
|
foreach (var row in rows)
|
||||||
|
applies.Add(await BuildApplyEntryAsync(row.Id, row.FromViewerId, row.CreatedAt, row.MissionType, ct));
|
||||||
|
|
||||||
|
return new ReceiveApplyInfoResult(applies, ApproveApplyCount: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SendApplyInfoResult> GetSendAppliesAsync(long viewerId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = await _db.ViewerFriendApplies
|
||||||
|
.Where(a => a.FromViewerId == viewerId)
|
||||||
|
.OrderBy(a => a.CreatedAt).ThenBy(a => a.Id)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var applies = new List<FriendApplyEntry>(rows.Count);
|
||||||
|
foreach (var row in rows)
|
||||||
|
applies.Add(await BuildApplyEntryAsync(row.Id, row.ToViewerId, row.CreatedAt, row.MissionType, ct));
|
||||||
|
|
||||||
|
int remaining = Math.Max(0, SendApplyMaxCount - rows.Count);
|
||||||
|
return new SendApplyInfoResult(applies, remaining, SendApplyMaxCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PlayedTogetherResult> GetPlayedTogetherAsync(long viewerId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var rows = await _db.ViewerPlayedTogethers
|
||||||
|
.Where(p => p.OwnerViewerId == viewerId)
|
||||||
|
.OrderByDescending(p => p.PlayedAt)
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
var entries = new List<PlayedTogetherEntry>(rows.Count);
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
var opp = await LoadViewerProjectionAsync(row.OpponentViewerId, ct);
|
||||||
|
if (opp is null) continue; // opponent deleted; skip the dead row
|
||||||
|
|
||||||
|
bool isFriend = await _db.ViewerFriends.AsNoTracking()
|
||||||
|
.AnyAsync(f => f.OwnerViewerId == viewerId && f.FriendViewerId == row.OpponentViewerId, ct);
|
||||||
|
|
||||||
|
int friendStatus = 0;
|
||||||
|
int friendApplyId = 0;
|
||||||
|
if (isFriend)
|
||||||
|
{
|
||||||
|
friendStatus = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var sent = await _db.ViewerFriendApplies.AsNoTracking()
|
||||||
|
.Where(a => a.FromViewerId == viewerId && a.ToViewerId == row.OpponentViewerId)
|
||||||
|
.Select(a => (int?)a.Id).FirstOrDefaultAsync(ct);
|
||||||
|
if (sent is { } sId) { friendStatus = 2; friendApplyId = sId; }
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var recv = await _db.ViewerFriendApplies.AsNoTracking()
|
||||||
|
.Where(a => a.FromViewerId == row.OpponentViewerId && a.ToViewerId == viewerId)
|
||||||
|
.Select(a => (int?)a.Id).FirstOrDefaultAsync(ct);
|
||||||
|
if (recv is { } rId) { friendStatus = 3; friendApplyId = rId; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.Add(new PlayedTogetherEntry(
|
||||||
|
(int)opp.Id,
|
||||||
|
opp.DisplayName,
|
||||||
|
opp.CountryCode,
|
||||||
|
ResolveRank(opp.DisplayName),
|
||||||
|
opp.EmblemId,
|
||||||
|
opp.DegreeId,
|
||||||
|
FormatWireTimestamp(opp.LastLogin),
|
||||||
|
FormatWireTimestamp(row.PlayedAt),
|
||||||
|
friendStatus,
|
||||||
|
friendApplyId,
|
||||||
|
row.PlayedMode,
|
||||||
|
row.BattleType,
|
||||||
|
row.DeckFormat,
|
||||||
|
row.TwoPickType));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PlayedTogetherResult(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<FriendEntry?> SearchAsync(long viewerId, int targetViewerId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (targetViewerId == (int)viewerId) return null;
|
||||||
|
return await BuildFriendEntryAsync(targetViewerId, ct);
|
||||||
|
}
|
||||||
|
|
||||||
public Task SendApplyAsync(long viewerId, int targetViewerId, CancellationToken ct) =>
|
public Task SendApplyAsync(long viewerId, int targetViewerId, CancellationToken ct) =>
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
@@ -66,4 +165,93 @@ public sealed class FriendService : IFriendService, IPlayedTogetherWriter
|
|||||||
|
|
||||||
public Task RecordAsync(long ownerViewerId, long opponentViewerId, BattleParticipationContext ctx, CancellationToken ct) =>
|
public Task RecordAsync(long ownerViewerId, long opponentViewerId, BattleParticipationContext ctx, CancellationToken ct) =>
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
// --- helpers ---
|
||||||
|
|
||||||
|
private sealed record ViewerProjection(
|
||||||
|
long Id,
|
||||||
|
string DisplayName,
|
||||||
|
DateTime LastLogin,
|
||||||
|
string CountryCode,
|
||||||
|
long EmblemId,
|
||||||
|
int DegreeId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads a Viewer with Info + cosmetic nav refs, then projects to a slim record.
|
||||||
|
/// We materialise the full entity rather than using Select() because EF Core
|
||||||
|
/// ignores Include/ThenInclude when a Select projection is present.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<ViewerProjection?> LoadViewerProjectionAsync(long viewerId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var v = await _db.Viewers
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.Id == viewerId)
|
||||||
|
.Include(x => x.Info).ThenInclude(i => i.SelectedEmblem)
|
||||||
|
.Include(x => x.Info).ThenInclude(i => i.SelectedDegree)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
|
||||||
|
if (v is null) return null;
|
||||||
|
|
||||||
|
return new ViewerProjection(
|
||||||
|
v.Id,
|
||||||
|
v.DisplayName,
|
||||||
|
v.LastLogin,
|
||||||
|
v.Info.CountryCode,
|
||||||
|
v.Info.SelectedEmblem?.Id ?? 0,
|
||||||
|
v.Info.SelectedDegree?.Id ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<FriendEntry?> BuildFriendEntryAsync(long friendViewerId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var v = await LoadViewerProjectionAsync(friendViewerId, ct);
|
||||||
|
if (v is null) return null;
|
||||||
|
|
||||||
|
return new FriendEntry(
|
||||||
|
ViewerId: (int)v.Id,
|
||||||
|
Name: v.DisplayName,
|
||||||
|
CountryCode: v.CountryCode,
|
||||||
|
Rank: ResolveRank(v.DisplayName),
|
||||||
|
EmblemId: v.EmblemId,
|
||||||
|
DegreeId: v.DegreeId,
|
||||||
|
LastPlayTime: FormatWireTimestamp(v.LastLogin),
|
||||||
|
DeviceType: DefaultDeviceType,
|
||||||
|
MaxFriend: DefaultMaxFriend,
|
||||||
|
IsReceivedTwoPickMission: DefaultIsReceivedTwoPickMission,
|
||||||
|
Birth: DefaultBirth,
|
||||||
|
MissionChangeTime: DefaultMissionChangeTime,
|
||||||
|
MissionReceiveType: DefaultMissionReceiveType,
|
||||||
|
IsOfficial: DefaultIsOfficial,
|
||||||
|
IsOfficialMarkDisplayed: DefaultIsOfficialMarkDisplayed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<FriendApplyEntry> BuildApplyEntryAsync(int applyId, long otherViewerId, DateTime createdAt, int missionType, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var v = await LoadViewerProjectionAsync(otherViewerId, ct);
|
||||||
|
// If viewer was deleted between apply creation and now, emit a placeholder so the wire doesn't break.
|
||||||
|
var displayName = v?.DisplayName ?? string.Empty;
|
||||||
|
var lastLogin = v?.LastLogin ?? DateTime.UnixEpoch;
|
||||||
|
var countryCode = v?.CountryCode ?? string.Empty;
|
||||||
|
var emblemId = v?.EmblemId ?? 0;
|
||||||
|
var degreeId = v?.DegreeId ?? 0;
|
||||||
|
|
||||||
|
return new FriendApplyEntry(
|
||||||
|
Id: applyId,
|
||||||
|
ViewerId: (int)otherViewerId,
|
||||||
|
Name: displayName,
|
||||||
|
CountryCode: countryCode,
|
||||||
|
Rank: ResolveRank(displayName),
|
||||||
|
EmblemId: emblemId,
|
||||||
|
DegreeId: degreeId,
|
||||||
|
LastPlayTime: FormatWireTimestamp(lastLogin),
|
||||||
|
CreateTime: FormatWireTimestamp(createdAt),
|
||||||
|
MissionType: missionType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rank derivation. We don't track per-viewer rank yet; always 1. Hook here when rank data lands.
|
||||||
|
/// </summary>
|
||||||
|
private static int ResolveRank(string _) => 1;
|
||||||
|
|
||||||
|
private static string FormatWireTimestamp(DateTime dt) =>
|
||||||
|
dt.ToString("yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using SVSim.Database.Repositories.Pack;
|
|||||||
using SVSim.Database.Repositories.Story;
|
using SVSim.Database.Repositories.Story;
|
||||||
using SVSim.Database.Repositories.Viewer;
|
using SVSim.Database.Repositories.Viewer;
|
||||||
using SVSim.Database.Services;
|
using SVSim.Database.Services;
|
||||||
|
using SVSim.Database.Services.Friend;
|
||||||
using SVSim.EmulatedEntrypoint.Configuration;
|
using SVSim.EmulatedEntrypoint.Configuration;
|
||||||
using SVSim.EmulatedEntrypoint.Extensions;
|
using SVSim.EmulatedEntrypoint.Extensions;
|
||||||
using SVSim.EmulatedEntrypoint.Matching;
|
using SVSim.EmulatedEntrypoint.Matching;
|
||||||
@@ -112,6 +113,9 @@ public class Program
|
|||||||
builder.Services.AddSingleton<IRandom, SystemRandom>();
|
builder.Services.AddSingleton<IRandom, SystemRandom>();
|
||||||
builder.Services.AddSingleton<PuzzleMissionEvaluator>();
|
builder.Services.AddSingleton<PuzzleMissionEvaluator>();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<IFriendService, FriendService>();
|
||||||
|
builder.Services.AddScoped<IPlayedTogetherWriter, FriendService>();
|
||||||
|
|
||||||
// Deck-code mint/resolve uses IMemoryCache for ephemeral (3-min TTL) storage; no DB
|
// Deck-code mint/resolve uses IMemoryCache for ephemeral (3-min TTL) storage; no DB
|
||||||
// row, no migration. Singleton because the cache + RNG seam are process-wide.
|
// row, no migration. Singleton because the cache + RNG seam are process-wide.
|
||||||
builder.Services.AddMemoryCache();
|
builder.Services.AddMemoryCache();
|
||||||
|
|||||||
296
SVSim.UnitTests/Services/FriendServiceTests.cs
Normal file
296
SVSim.UnitTests/Services/FriendServiceTests.cs
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using SVSim.Database;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
using SVSim.Database.Services.Friend;
|
||||||
|
using SVSim.UnitTests.Infrastructure;
|
||||||
|
|
||||||
|
namespace SVSim.UnitTests.Services;
|
||||||
|
|
||||||
|
public class FriendServiceTests
|
||||||
|
{
|
||||||
|
private static async Task<long> SeedViewer(SVSimTestFactory factory, ulong steamId, string name = "Test Viewer")
|
||||||
|
=> await factory.SeedViewerAsync(steamId: steamId, displayName: name);
|
||||||
|
|
||||||
|
private static IFriendService Service(SVSimTestFactory factory, out IServiceScope scope)
|
||||||
|
{
|
||||||
|
scope = factory.Services.CreateScope();
|
||||||
|
return scope.ServiceProvider.GetRequiredService<IFriendService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SVSimDbContext Ctx(IServiceScope scope) =>
|
||||||
|
scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetFriendsAsync_returns_empty_for_fresh_viewer()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long viewerId = await SeedViewer(factory, 76_561_198_000_000_001UL);
|
||||||
|
|
||||||
|
var svc = Service(factory, out var scope);
|
||||||
|
using (scope)
|
||||||
|
{
|
||||||
|
var result = await svc.GetFriendsAsync(viewerId, default);
|
||||||
|
Assert.That(result.Friends, Is.Empty);
|
||||||
|
Assert.That(result.Count, Is.EqualTo(0));
|
||||||
|
Assert.That(result.MaxCount, Is.EqualTo(110));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetFriendsAsync_returns_15_field_entries_for_seeded_friend()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long owner = await SeedViewer(factory, 76_561_198_000_000_002UL, "Owner");
|
||||||
|
long friend = await SeedViewer(factory, 76_561_198_000_000_003UL, "Friend");
|
||||||
|
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var ctx = Ctx(scope);
|
||||||
|
ctx.ViewerFriends.Add(new ViewerFriend { OwnerViewerId = owner, FriendViewerId = friend, CreatedAt = DateTime.UtcNow });
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var svc = Service(factory, out var scope2);
|
||||||
|
using (scope2)
|
||||||
|
{
|
||||||
|
var result = await svc.GetFriendsAsync(owner, default);
|
||||||
|
Assert.That(result.Friends, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(result.Count, Is.EqualTo(1));
|
||||||
|
var entry = result.Friends[0];
|
||||||
|
Assert.That(entry.ViewerId, Is.EqualTo((int)friend));
|
||||||
|
Assert.That(entry.Name, Is.EqualTo("Friend"));
|
||||||
|
Assert.That(entry.DeviceType, Is.EqualTo("2"));
|
||||||
|
Assert.That(entry.MaxFriend, Is.EqualTo("110"));
|
||||||
|
Assert.That(entry.IsOfficial, Is.EqualTo("0"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetReceiveAppliesAsync_returns_incoming_with_correct_viewer_id_and_id()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long target = await SeedViewer(factory, 76_561_198_000_000_004UL, "Target");
|
||||||
|
long sender = await SeedViewer(factory, 76_561_198_000_000_005UL, "Sender");
|
||||||
|
|
||||||
|
int applyId;
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var ctx = Ctx(scope);
|
||||||
|
var apply = new ViewerFriendApply { FromViewerId = sender, ToViewerId = target, CreatedAt = DateTime.UtcNow };
|
||||||
|
ctx.ViewerFriendApplies.Add(apply);
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
applyId = apply.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var svc = Service(factory, out var scope2);
|
||||||
|
using (scope2)
|
||||||
|
{
|
||||||
|
var result = await svc.GetReceiveAppliesAsync(target, default);
|
||||||
|
Assert.That(result.ReceiveApplies, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(result.ReceiveApplies[0].Id, Is.EqualTo(applyId));
|
||||||
|
Assert.That(result.ReceiveApplies[0].ViewerId, Is.EqualTo((int)sender), "viewer_id is the SENDER's id");
|
||||||
|
Assert.That(result.ReceiveApplies[0].Name, Is.EqualTo("Sender"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetSendAppliesAsync_returns_outgoing_with_remaining_count()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long sender = await SeedViewer(factory, 76_561_198_000_000_006UL, "Sender");
|
||||||
|
long target = await SeedViewer(factory, 76_561_198_000_000_007UL, "Target");
|
||||||
|
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var ctx = Ctx(scope);
|
||||||
|
ctx.ViewerFriendApplies.Add(new ViewerFriendApply { FromViewerId = sender, ToViewerId = target, CreatedAt = DateTime.UtcNow });
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var svc = Service(factory, out var scope2);
|
||||||
|
using (scope2)
|
||||||
|
{
|
||||||
|
var result = await svc.GetSendAppliesAsync(sender, default);
|
||||||
|
Assert.That(result.SendApplies, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(result.SendApplyMaxCount, Is.EqualTo(110));
|
||||||
|
Assert.That(result.RemainingApplyCount, Is.EqualTo(109));
|
||||||
|
Assert.That(result.SendApplies[0].ViewerId, Is.EqualTo((int)target), "viewer_id is the TARGET's id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetPlayedTogetherAsync_returns_empty_for_fresh_viewer()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long viewerId = await SeedViewer(factory, 76_561_198_000_000_008UL);
|
||||||
|
|
||||||
|
var svc = Service(factory, out var scope);
|
||||||
|
using (scope)
|
||||||
|
{
|
||||||
|
var result = await svc.GetPlayedTogetherAsync(viewerId, default);
|
||||||
|
Assert.That(result.Histories, Is.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetPlayedTogetherAsync_computes_friend_status_NO_ACTION_for_stranger()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long me = await SeedViewer(factory, 76_561_198_000_000_009UL, "Me");
|
||||||
|
long opponent = await SeedViewer(factory, 76_561_198_000_000_010UL, "Opponent");
|
||||||
|
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var ctx = Ctx(scope);
|
||||||
|
ctx.ViewerPlayedTogethers.Add(new ViewerPlayedTogether
|
||||||
|
{
|
||||||
|
OwnerViewerId = me, OpponentViewerId = opponent,
|
||||||
|
PlayedAt = DateTime.UtcNow, PlayedMode = 1, BattleType = 2, DeckFormat = 3, TwoPickType = 4,
|
||||||
|
});
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var svc = Service(factory, out var scope2);
|
||||||
|
using (scope2)
|
||||||
|
{
|
||||||
|
var result = await svc.GetPlayedTogetherAsync(me, default);
|
||||||
|
Assert.That(result.Histories, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(result.Histories[0].FriendStatus, Is.EqualTo(0), "no apply, no friendship → NO_ACTION");
|
||||||
|
Assert.That(result.Histories[0].FriendApplyId, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetPlayedTogetherAsync_computes_friend_status_IS_FRIEND_for_friend()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long me = await SeedViewer(factory, 76_561_198_000_000_011UL, "Me");
|
||||||
|
long friend = await SeedViewer(factory, 76_561_198_000_000_012UL, "Friend");
|
||||||
|
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var ctx = Ctx(scope);
|
||||||
|
ctx.ViewerFriends.Add(new ViewerFriend { OwnerViewerId = me, FriendViewerId = friend, CreatedAt = DateTime.UtcNow });
|
||||||
|
ctx.ViewerPlayedTogethers.Add(new ViewerPlayedTogether
|
||||||
|
{
|
||||||
|
OwnerViewerId = me, OpponentViewerId = friend,
|
||||||
|
PlayedAt = DateTime.UtcNow, PlayedMode = 1, BattleType = 2, DeckFormat = 3, TwoPickType = 4,
|
||||||
|
});
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var svc = Service(factory, out var scope2);
|
||||||
|
using (scope2)
|
||||||
|
{
|
||||||
|
var result = await svc.GetPlayedTogetherAsync(me, default);
|
||||||
|
Assert.That(result.Histories[0].FriendStatus, Is.EqualTo(1), "IS_FRIEND");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetPlayedTogetherAsync_computes_friend_status_IS_SEND_with_apply_id()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long me = await SeedViewer(factory, 76_561_198_000_000_013UL, "Me");
|
||||||
|
long target = await SeedViewer(factory, 76_561_198_000_000_014UL, "Target");
|
||||||
|
|
||||||
|
int applyId;
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var ctx = Ctx(scope);
|
||||||
|
var apply = new ViewerFriendApply { FromViewerId = me, ToViewerId = target, CreatedAt = DateTime.UtcNow };
|
||||||
|
ctx.ViewerFriendApplies.Add(apply);
|
||||||
|
ctx.ViewerPlayedTogethers.Add(new ViewerPlayedTogether
|
||||||
|
{
|
||||||
|
OwnerViewerId = me, OpponentViewerId = target,
|
||||||
|
PlayedAt = DateTime.UtcNow, PlayedMode = 1, BattleType = 2, DeckFormat = 3, TwoPickType = 4,
|
||||||
|
});
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
applyId = apply.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var svc = Service(factory, out var scope2);
|
||||||
|
using (scope2)
|
||||||
|
{
|
||||||
|
var result = await svc.GetPlayedTogetherAsync(me, default);
|
||||||
|
Assert.That(result.Histories[0].FriendStatus, Is.EqualTo(2), "IS_SEND");
|
||||||
|
Assert.That(result.Histories[0].FriendApplyId, Is.EqualTo(applyId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetPlayedTogetherAsync_computes_friend_status_IS_RECEIVED_with_apply_id()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long me = await SeedViewer(factory, 76_561_198_000_000_015UL, "Me");
|
||||||
|
long sender = await SeedViewer(factory, 76_561_198_000_000_016UL, "Sender");
|
||||||
|
|
||||||
|
int applyId;
|
||||||
|
using (var scope = factory.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var ctx = Ctx(scope);
|
||||||
|
var apply = new ViewerFriendApply { FromViewerId = sender, ToViewerId = me, CreatedAt = DateTime.UtcNow };
|
||||||
|
ctx.ViewerFriendApplies.Add(apply);
|
||||||
|
ctx.ViewerPlayedTogethers.Add(new ViewerPlayedTogether
|
||||||
|
{
|
||||||
|
OwnerViewerId = me, OpponentViewerId = sender,
|
||||||
|
PlayedAt = DateTime.UtcNow, PlayedMode = 1, BattleType = 2, DeckFormat = 3, TwoPickType = 4,
|
||||||
|
});
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
applyId = apply.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var svc = Service(factory, out var scope2);
|
||||||
|
using (scope2)
|
||||||
|
{
|
||||||
|
var result = await svc.GetPlayedTogetherAsync(me, default);
|
||||||
|
Assert.That(result.Histories[0].FriendStatus, Is.EqualTo(3), "IS_RECEIVED");
|
||||||
|
Assert.That(result.Histories[0].FriendApplyId, Is.EqualTo(applyId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task SearchAsync_returns_entry_for_existing_viewer()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long me = await SeedViewer(factory, 76_561_198_000_000_017UL, "Me");
|
||||||
|
long target = await SeedViewer(factory, 76_561_198_000_000_018UL, "Target");
|
||||||
|
|
||||||
|
var svc = Service(factory, out var scope);
|
||||||
|
using (scope)
|
||||||
|
{
|
||||||
|
var result = await svc.SearchAsync(me, (int)target, default);
|
||||||
|
Assert.That(result, Is.Not.Null);
|
||||||
|
Assert.That(result!.Name, Is.EqualTo("Target"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task SearchAsync_returns_null_for_self_search()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long me = await SeedViewer(factory, 76_561_198_000_000_019UL);
|
||||||
|
|
||||||
|
var svc = Service(factory, out var scope);
|
||||||
|
using (scope)
|
||||||
|
{
|
||||||
|
var result = await svc.SearchAsync(me, (int)me, default);
|
||||||
|
Assert.That(result, Is.Null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task SearchAsync_returns_null_for_unknown_viewer_id()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
long me = await SeedViewer(factory, 76_561_198_000_000_020UL);
|
||||||
|
|
||||||
|
var svc = Service(factory, out var scope);
|
||||||
|
using (scope)
|
||||||
|
{
|
||||||
|
var result = await svc.SearchAsync(me, 999_999_999, default);
|
||||||
|
Assert.That(result, Is.Null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user