profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

main default branch 429 files Expires Sep 13, 2026, 9:06 AM
TripServiceTests.cs 3,861 bytes
1 using App.BLL.DTO;
2 using App.BLL.Services;
3 using App.Domain;
4 using App.Domain.Contracts;
5 using FluentAssertions;
6 using Moq;
7
8 namespace App.Tests.BLL;
9
10 public class TripServiceTests
11 {
12 private readonly Mock<IAppUnitOfWork> _uow = new();
13 private readonly Mock<ITripRepository> _trips = new();
14 private readonly Mock<ITripParticipantRepository> _participants = new();
15 private readonly Mock<ISettlementService> _settlement = new();
16 private readonly TripService _sut;
17
18 public TripServiceTests()
19 {
20 _uow.Setup(u => u.Trips).Returns(_trips.Object);
21 _uow.Setup(u => u.TripParticipants).Returns(_participants.Object);
22 _sut = new TripService(_uow.Object, _settlement.Object);
23 }
24
25 [Fact]
26 public async Task CreateTripAsync_AddsTripAndOrganizerParticipant_ThenSaves()
27 {
28 // Arrange
29 var userId = Guid.NewGuid();
30 var dto = new TripBllDto { Name = "Paris", DefaultCurrencyId = Guid.NewGuid() };
31
32 _trips.Setup(r => r.Add(It.IsAny<Trip>())).Returns((Trip t) => t);
33 _participants.Setup(r => r.Add(It.IsAny<TripParticipant>())).Returns((TripParticipant p) => p);
34
35 // Act
36 var result = await _sut.CreateTripAsync(dto, userId);
37
38 // Assert
39 result.Should().NotBeNull();
40 result.Name.Should().Be("Paris");
41 result.CreatedById.Should().Be(userId);
42 result.Status.Should().Be(ETripStatus.Active);
43
44 _trips.Verify(r => r.Add(It.Is<Trip>(t => t.CreatedById == userId)), Times.Once);
45 _participants.Verify(r => r.Add(It.Is<TripParticipant>(
46 p => p.UserId == userId && p.Role == EParticipantRole.Organizer)), Times.Once);
47 _uow.Verify(u => u.SaveChangesAsync(), Times.Once);
48 }
49
50 [Fact]
51 public async Task IsParticipantAsync_DelegatesToParticipantRepo()
52 {
53 var tripId = Guid.NewGuid();
54 var userId = Guid.NewGuid();
55 _participants.Setup(r => r.IsParticipantAsync(tripId, userId)).ReturnsAsync(true);
56
57 var result = await _sut.IsParticipantAsync(tripId, userId);
58
59 result.Should().BeTrue();
60 _participants.Verify(r => r.IsParticipantAsync(tripId, userId), Times.Once);
61 }
62
63 [Fact]
64 public async Task IsOrganizerAsync_DelegatesToParticipantRepo()
65 {
66 var tripId = Guid.NewGuid();
67 var userId = Guid.NewGuid();
68 _participants.Setup(r => r.IsOrganizerAsync(tripId, userId)).ReturnsAsync(true);
69
70 var result = await _sut.IsOrganizerAsync(tripId, userId);
71
72 result.Should().BeTrue();
73 _participants.Verify(r => r.IsOrganizerAsync(tripId, userId), Times.Once);
74 }
75
76 // ----- Sad-path / IDOR negatives -----
77
78 [Fact]
79 public async Task GetByIdAsync_WhenUserIsNotParticipant_ReturnsNull()
80 {
81 // Arrange — IDOR enforcement: non-participants must not see the trip
82 var tripId = Guid.NewGuid();
83 var userId = Guid.NewGuid();
84 _participants.Setup(r => r.IsParticipantAsync(tripId, userId)).ReturnsAsync(false);
85
86 // Act
87 var result = await _sut.GetByIdAsync(tripId, userId);
88
89 // Assert
90 result.Should().BeNull();
91 // Crucial — repo must not even be called when user has no access
92 _trips.Verify(r => r.GetByIdAsync(It.IsAny<Guid>()), Times.Never);
93 }
94
95 [Fact]
96 public async Task DeleteAsync_WhenUserIsNotOrganizer_ReturnsFalseAndDoesNotDelete()
97 {
98 // Arrange
99 var tripId = Guid.NewGuid();
100 var userId = Guid.NewGuid();
101 _participants.Setup(r => r.IsOrganizerAsync(tripId, userId)).ReturnsAsync(false);
102
103 // Act
104 var result = await _sut.DeleteAsync(tripId, userId);
105
106 // Assert
107 result.Should().BeFalse();
108 _trips.Verify(r => r.RemoveAsync(It.IsAny<Guid>()), Times.Never);
109 _uow.Verify(u => u.SaveChangesAsync(), Times.Never);
110 }
111 }
112