Skip to content

Commit eb05809

Browse files
authored
feat(Duely): get user duels history (#91)
1 parent 9772ec1 commit eb05809

File tree

4 files changed

+200
-0
lines changed

4 files changed

+200
-0
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.Text.Json.Serialization;
2+
using Duely.Domain.Models;
3+
4+
namespace Duely.Application.UseCases.Dtos;
5+
6+
public sealed class DuelHistoryItemDto
7+
{
8+
[JsonPropertyName("id")]
9+
public required int Id { get; init; }
10+
11+
[JsonPropertyName("status"), JsonConverter(typeof(JsonStringEnumConverter))]
12+
public required DuelStatus Status { get; init; }
13+
14+
[JsonPropertyName("opponent_nickname")]
15+
public required string OpponentNickname { get; init; }
16+
17+
[JsonPropertyName("winner_nickname")]
18+
public string? WinnerNickname { get; init; }
19+
20+
[JsonPropertyName("start_time")]
21+
public required DateTime StartTime { get; init; }
22+
23+
[JsonPropertyName("end_time")]
24+
public DateTime? EndTime { get; init; }
25+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using Duely.Application.UseCases.Dtos;
2+
using Duely.Application.UseCases.Errors;
3+
using Duely.Domain.Models;
4+
using Duely.Infrastructure.DataAccess.EntityFramework;
5+
using FluentResults;
6+
using MediatR;
7+
using Microsoft.EntityFrameworkCore;
8+
9+
namespace Duely.Application.UseCases.Features.Duels;
10+
11+
public sealed class GetUserDuelsQuery : IRequest<Result<List<DuelHistoryItemDto>>>
12+
{
13+
public required int UserId { get; init; }
14+
}
15+
16+
public sealed class GetUserDuelsHandler(Context context)
17+
: IRequestHandler<GetUserDuelsQuery, Result<List<DuelHistoryItemDto>>>
18+
{
19+
public async Task<Result<List<DuelHistoryItemDto>>> Handle(GetUserDuelsQuery query, CancellationToken cancellationToken)
20+
{
21+
var userExists = await context.Users.AnyAsync(u => u.Id == query.UserId, cancellationToken);
22+
23+
if (!userExists)
24+
{
25+
return new EntityNotFoundError(nameof(User), nameof(User.Id), query.UserId);
26+
}
27+
var duels = await context.Duels
28+
.Where(d =>(d.User1 != null && d.User1.Id == query.UserId) || (d.User2 != null && d.User2.Id == query.UserId))
29+
.OrderByDescending(d => d.StartTime)
30+
.Select(d => new DuelHistoryItemDto
31+
{
32+
Id = d.Id,
33+
Status = d.Status,
34+
StartTime = d.StartTime,
35+
EndTime = d.EndTime,
36+
OpponentNickname = d.User1 != null && d.User1.Id == query.UserId
37+
? d.User2!.Nickname
38+
: d.User1!.Nickname,
39+
WinnerNickname = d.Status == DuelStatus.Finished && d.Winner != null
40+
? d.Winner.Nickname
41+
: null
42+
})
43+
.ToListAsync(cancellationToken);
44+
45+
return Result.Ok(duels);
46+
}
47+
}

Duely/src/Duely.Infrastructure.Api.Http/Controllers/DuelsController.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ public async Task<ActionResult<DuelDto>> GetAsync(
3434
return this.HandleResult(result);
3535
}
3636

37+
[HttpGet]
38+
public async Task<ActionResult<List<DuelHistoryItemDto>>> GetUserDuelsHistoryAsync(
39+
CancellationToken cancellationToken)
40+
{
41+
var query = new GetUserDuelsQuery
42+
{
43+
UserId = userContext.UserId
44+
};
45+
46+
var result = await mediator.Send(query, cancellationToken);
47+
return this.HandleResult(result);
48+
}
49+
3750
[HttpGet("connect")]
3851
public async Task Connect(CancellationToken cancellationToken)
3952
{
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Duely.Application.Tests.TestHelpers;
6+
using Duely.Application.UseCases.Errors;
7+
using Duely.Application.UseCases.Features.Duels;
8+
using Duely.Domain.Models;
9+
using FluentAssertions;
10+
using Xunit;
11+
12+
public class GetUserDuelsHandlerTests : ContextBasedTest
13+
{
14+
[Fact]
15+
public async Task NotFound_when_user_does_not_exist()
16+
{
17+
var ctx = Context;
18+
19+
var handler = new GetUserDuelsHandler(ctx);
20+
var res = await handler.Handle(
21+
new GetUserDuelsQuery { UserId = 42 },
22+
CancellationToken.None);
23+
24+
res.IsFailed.Should().BeTrue();
25+
res.Errors.Should().ContainSingle(e => e is EntityNotFoundError);
26+
}
27+
28+
[Fact]
29+
public async Task Returns_empty_list_when_user_has_no_duels()
30+
{
31+
var ctx = Context;
32+
33+
var user = EntityFactory.MakeUser(1, "u1");
34+
ctx.Users.Add(user);
35+
await ctx.SaveChangesAsync();
36+
37+
var handler = new GetUserDuelsHandler(ctx);
38+
var res = await handler.Handle(
39+
new GetUserDuelsQuery { UserId = 1 },
40+
CancellationToken.None);
41+
42+
res.IsSuccess.Should().BeTrue();
43+
res.Value.Should().BeEmpty();
44+
}
45+
46+
[Fact]
47+
public async Task Returns_sorted_history_with_correct_opponent_and_winner_for_user1()
48+
{
49+
var ctx = Context;
50+
51+
var u1 = EntityFactory.MakeUser(1, "u1");
52+
var u2 = EntityFactory.MakeUser(2, "u2");
53+
var u3 = EntityFactory.MakeUser(3, "u3");
54+
ctx.Users.AddRange(u1, u2, u3);
55+
var duel1 = EntityFactory.MakeDuel(10, u1, u2, "TASK1");
56+
duel1.Status = DuelStatus.Finished;
57+
duel1.EndTime = DateTime.UtcNow.AddMinutes(-3);
58+
duel1.Winner = u1;
59+
var duel2 = EntityFactory.MakeDuel(11, u2, u1, "TASK2");
60+
duel2.Status = DuelStatus.InProgress;
61+
var duelOther = EntityFactory.MakeDuel(12, u2, u3, "TASK3");
62+
63+
ctx.Duels.AddRange(duel1, duel2, duelOther);
64+
await ctx.SaveChangesAsync();
65+
66+
var handler = new GetUserDuelsHandler(ctx);
67+
var res = await handler.Handle(
68+
new GetUserDuelsQuery { UserId = 1 },
69+
CancellationToken.None);
70+
71+
res.IsSuccess.Should().BeTrue();
72+
73+
var history = res.Value;
74+
history.Should().HaveCount(2);
75+
history.Select(h => h.Id).Should().ContainInOrder(11, 10);
76+
77+
var first = history[0];
78+
var second = history[1];
79+
80+
first.OpponentNickname.Should().Be("u2");
81+
second.OpponentNickname.Should().Be("u2");
82+
first.Status.Should().Be(DuelStatus.InProgress);
83+
first.WinnerNickname.Should().BeNull();
84+
second.Status.Should().Be(DuelStatus.Finished);
85+
second.WinnerNickname.Should().Be("u1");
86+
}
87+
88+
[Fact]
89+
public async Task Opponent_and_winner_are_correct_for_user2_side()
90+
{
91+
var ctx = Context;
92+
93+
var u1 = EntityFactory.MakeUser(1, "u1");
94+
var u2 = EntityFactory.MakeUser(2, "u2");
95+
ctx.Users.AddRange(u1, u2);
96+
97+
var duel = EntityFactory.MakeDuel(20, u1, u2, "TASK");
98+
duel.Status = DuelStatus.Finished;
99+
duel.EndTime = DateTime.UtcNow;
100+
duel.Winner = u1;
101+
102+
ctx.Duels.Add(duel);
103+
await ctx.SaveChangesAsync();
104+
105+
var handler = new GetUserDuelsHandler(ctx);
106+
var res = await handler.Handle(
107+
new GetUserDuelsQuery { UserId = 2 },
108+
CancellationToken.None);
109+
110+
res.IsSuccess.Should().BeTrue();
111+
var item = res.Value.Single();
112+
item.OpponentNickname.Should().Be("u1");
113+
item.WinnerNickname.Should().Be("u1");
114+
}
115+
}

0 commit comments

Comments
 (0)