/ GUNRPG.Tests / OperatorAggregateTests.cs
OperatorAggregateTests.cs
1 using GUNRPG.Core.Operators; 2 using Xunit; 3 4 namespace GUNRPG.Tests; 5 6 public class OperatorAggregateTests 7 { 8 [Fact] 9 public void Create_ShouldInitializeFromCreatedEvent() 10 { 11 // Arrange 12 var operatorId = OperatorId.NewId(); 13 var createdEvent = new OperatorCreatedEvent(operatorId, "TestOperator"); 14 15 // Act 16 var aggregate = OperatorAggregate.Create(createdEvent); 17 18 // Assert 19 Assert.Equal(operatorId, aggregate.Id); 20 Assert.Equal("TestOperator", aggregate.Name); 21 Assert.Equal(0, aggregate.TotalXp); 22 Assert.Equal(100f, aggregate.MaxHealth); 23 Assert.Equal(100f, aggregate.CurrentHealth); 24 Assert.Equal(0, aggregate.CurrentSequence); 25 } 26 27 [Fact] 28 public void FromEvents_ShouldReplayEventsInOrder() 29 { 30 // Arrange 31 var operatorId = OperatorId.NewId(); 32 var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator"); 33 var evt2 = new XpGainedEvent(operatorId, 1, 100, "Victory", evt1.Hash); 34 var evt3 = new XpGainedEvent(operatorId, 2, 50, "Survived", evt2.Hash); 35 var events = new List<OperatorEvent> { evt1, evt2, evt3 }; 36 37 // Act 38 var aggregate = OperatorAggregate.FromEvents(events); 39 40 // Assert 41 Assert.Equal(150, aggregate.TotalXp); 42 Assert.Equal(2, aggregate.CurrentSequence); 43 } 44 45 [Fact] 46 public void FromEvents_ShouldThrowOnEmptyEventList() 47 { 48 // Arrange 49 var events = new List<OperatorEvent>(); 50 51 // Act & Assert 52 var ex = Assert.Throws<InvalidOperationException>(() => OperatorAggregate.FromEvents(events)); 53 Assert.Contains("empty event list", ex.Message); 54 } 55 56 [Fact] 57 public void FromEvents_ShouldRollbackOnBrokenChain() 58 { 59 // Arrange 60 var operatorId = OperatorId.NewId(); 61 var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator"); 62 var evt2 = new XpGainedEvent(operatorId, 1, 100, "Victory", "wrong_hash"); 63 var events = new List<OperatorEvent> { evt1, evt2 }; 64 65 // Act - Should roll back to evt1 only 66 var aggregate = OperatorAggregate.FromEvents(events); 67 68 // Assert - Only first event should be applied 69 Assert.Equal(0, aggregate.TotalXp); 70 Assert.Equal(0, aggregate.CurrentSequence); 71 } 72 73 [Fact] 74 public void FromEvents_ShouldApplyXpGainedCorrectly() 75 { 76 // Arrange 77 var operatorId = OperatorId.NewId(); 78 var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator"); 79 var evt2 = new XpGainedEvent(operatorId, 1, 250, "Victory", evt1.Hash); 80 var events = new List<OperatorEvent> { evt1, evt2 }; 81 82 // Act 83 var aggregate = OperatorAggregate.FromEvents(events); 84 85 // Assert 86 Assert.Equal(250, aggregate.TotalXp); 87 } 88 89 [Fact] 90 public void FromEvents_ShouldApplyWoundsTreatedCorrectly() 91 { 92 // Arrange 93 var operatorId = OperatorId.NewId(); 94 var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator"); 95 var evt2 = new WoundsTreatedEvent(operatorId, 1, 30f, evt1.Hash); 96 var events = new List<OperatorEvent> { evt1, evt2 }; 97 98 // Act 99 var aggregate = OperatorAggregate.FromEvents(events); 100 101 // Assert 102 Assert.Equal(100f, aggregate.CurrentHealth); // Already at max 103 } 104 105 [Fact] 106 public void FromEvents_ShouldApplyLoadoutChangedCorrectly() 107 { 108 // Arrange 109 var operatorId = OperatorId.NewId(); 110 var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator"); 111 var evt2 = new LoadoutChangedEvent(operatorId, 1, "M4A1", evt1.Hash); 112 var events = new List<OperatorEvent> { evt1, evt2 }; 113 114 // Act 115 var aggregate = OperatorAggregate.FromEvents(events); 116 117 // Assert 118 Assert.Equal("M4A1", aggregate.EquippedWeaponName); 119 } 120 121 [Fact] 122 public void FromEvents_ShouldApplyPerkUnlockedCorrectly() 123 { 124 // Arrange 125 var operatorId = OperatorId.NewId(); 126 var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator"); 127 var evt2 = new PerkUnlockedEvent(operatorId, 1, "Fast Reload", evt1.Hash); 128 var evt3 = new PerkUnlockedEvent(operatorId, 2, "Double Tap", evt2.Hash); 129 var events = new List<OperatorEvent> { evt1, evt2, evt3 }; 130 131 // Act 132 var aggregate = OperatorAggregate.FromEvents(events); 133 134 // Assert 135 Assert.Equal(2, aggregate.UnlockedPerks.Count); 136 Assert.Contains("Fast Reload", aggregate.UnlockedPerks); 137 Assert.Contains("Double Tap", aggregate.UnlockedPerks); 138 } 139 140 [Fact] 141 public void GetLastEventHash_ShouldReturnEmptyForNewAggregate() 142 { 143 // Arrange 144 var operatorId = OperatorId.NewId(); 145 var evt = new OperatorCreatedEvent(operatorId, "TestOperator"); 146 var aggregate = OperatorAggregate.Create(evt); 147 148 // Act 149 var lastHash = aggregate.GetLastEventHash(); 150 151 // Assert 152 Assert.NotEmpty(lastHash); 153 Assert.Equal(evt.Hash, lastHash); 154 } 155 156 [Fact] 157 public void GetLastEventHash_ShouldReturnLatestHash() 158 { 159 // Arrange 160 var operatorId = OperatorId.NewId(); 161 var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator"); 162 var evt2 = new XpGainedEvent(operatorId, 1, 100, "Victory", evt1.Hash); 163 var events = new List<OperatorEvent> { evt1, evt2 }; 164 var aggregate = OperatorAggregate.FromEvents(events); 165 166 // Act 167 var lastHash = aggregate.GetLastEventHash(); 168 169 // Assert 170 Assert.Equal(evt2.Hash, lastHash); 171 } 172 173 [Fact] 174 public void CreateCombatSnapshot_ShouldCopyBasicStats() 175 { 176 // Arrange 177 var operatorId = OperatorId.NewId(); 178 var evt = new OperatorCreatedEvent(operatorId, "TestOperator"); 179 var aggregate = OperatorAggregate.Create(evt); 180 181 // Act 182 var snapshot = aggregate.CreateCombatSnapshot(); 183 184 // Assert 185 Assert.Equal(operatorId.Value, snapshot.Id); 186 Assert.Equal("TestOperator", snapshot.Name); 187 Assert.Equal(100f, snapshot.MaxHealth); 188 Assert.Equal(100f, snapshot.Health); 189 } 190 191 [Fact] 192 public void WoundsTreated_ShouldNotExceedMaxHealth() 193 { 194 // Arrange 195 var operatorId = OperatorId.NewId(); 196 var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator"); 197 198 // Apply healing to an operator at full health 199 var evt2 = new WoundsTreatedEvent(operatorId, 1, 50f, evt1.Hash); 200 201 // Act 202 var events = new List<OperatorEvent> { evt1, evt2 }; 203 var restoredAggregate = OperatorAggregate.FromEvents(events); 204 205 // Assert - Health should remain capped at max even with excess healing 206 Assert.Equal(100f, restoredAggregate.CurrentHealth); 207 } 208 209 [Fact] 210 public void CombatVictory_ShouldNotIncrementStreak() 211 { 212 // CombatVictoryEvent clears the active combat session but does NOT increment ExfilStreak. 213 // ExfilStreak only increments when the infil is completed successfully (InfilEndedEvent with wasSuccessful=true). 214 215 // Arrange 216 var operatorId = OperatorId.NewId(); 217 var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator"); 218 var evt2 = new CombatVictoryEvent(operatorId, 1, evt1.Hash); 219 var evt3 = new CombatVictoryEvent(operatorId, 2, evt2.Hash); 220 221 // Act 222 var events = new List<OperatorEvent> { evt1, evt2, evt3 }; 223 var aggregate = OperatorAggregate.FromEvents(events); 224 225 // Assert 226 Assert.Equal(0, aggregate.ExfilStreak); 227 Assert.False(aggregate.IsDead); 228 } 229 230 [Fact] 231 public void ExfilFailed_ShouldResetStreak() 232 { 233 // Arrange 234 var operatorId = OperatorId.NewId(); 235 var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator"); 236 var evt2 = new CombatVictoryEvent(operatorId, 1, evt1.Hash); 237 var evt3 = new CombatVictoryEvent(operatorId, 2, evt2.Hash); 238 var evt4 = new ExfilFailedEvent(operatorId, 3, "Retreat", evt3.Hash); 239 240 // Act 241 var events = new List<OperatorEvent> { evt1, evt2, evt3, evt4 }; 242 var aggregate = OperatorAggregate.FromEvents(events); 243 244 // Assert 245 Assert.Equal(0, aggregate.ExfilStreak); 246 Assert.False(aggregate.IsDead); 247 } 248 249 [Fact] 250 public void OperatorDied_ShouldRespawnWithFullHealthAndResetStreak() 251 { 252 // Arrange 253 var operatorId = OperatorId.NewId(); 254 var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator"); 255 var evt2 = new CombatVictoryEvent(operatorId, 1, evt1.Hash); 256 var evt3 = new OperatorDiedEvent(operatorId, 2, "Combat casualty", evt2.Hash); 257 258 // Act 259 var events = new List<OperatorEvent> { evt1, evt2, evt3 }; 260 var aggregate = OperatorAggregate.FromEvents(events); 261 262 // Assert 263 Assert.False(aggregate.IsDead); // Operator respawns after death 264 Assert.Equal(aggregate.MaxHealth, aggregate.CurrentHealth); // Health restored to full after death 265 Assert.Equal(0, aggregate.ExfilStreak); 266 } 267 268 [Fact] 269 public void CombatVictory_ShouldClearActiveSessionId() 270 { 271 // Arrange 272 var operatorId = OperatorId.NewId(); 273 var sessionId = Guid.NewGuid(); 274 var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator"); 275 var evt2 = new InfilStartedEvent(operatorId, 1, sessionId, "AK-47", DateTimeOffset.UtcNow, evt1.Hash); 276 var evt3 = new CombatVictoryEvent(operatorId, 2, evt2.Hash); 277 278 // Act 279 var events = new List<OperatorEvent> { evt1, evt2, evt3 }; 280 var aggregate = OperatorAggregate.FromEvents(events); 281 282 // Assert 283 Assert.Equal(OperatorMode.Infil, aggregate.CurrentMode); // Should stay in Infil mode 284 Assert.Equal(0, aggregate.ExfilStreak); // Streak NOT incremented (only increments on infil completion) 285 Assert.Null(aggregate.ActiveCombatSessionId); // ActiveSessionId cleared to prevent auto-resume of completed session 286 } 287 288 [Fact] 289 public void FromEvents_ShouldRollbackOnHashFailure() 290 { 291 // Arrange 292 var operatorId = OperatorId.NewId(); 293 var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator"); 294 var evt2 = new XpGainedEvent(operatorId, 1, 100, "Victory", evt1.Hash); 295 296 // Create a corrupted event with wrong hash 297 var evt3 = new XpGainedEvent(operatorId, 2, 50, "Survived", evt2.Hash); 298 // Manually corrupt the event by creating a new one with same data but we'll simulate corruption 299 // by passing wrong previous hash to the next event 300 var evt4 = new XpGainedEvent(operatorId, 3, 25, "Bonus", "corrupted_hash"); 301 302 var events = new List<OperatorEvent> { evt1, evt2, evt3, evt4 }; 303 304 // Act - The aggregate should only replay valid events up to evt3 305 var aggregate = OperatorAggregate.FromEvents(events); 306 307 // Assert - Should have rolled back to last valid event (evt3) 308 Assert.Equal(150, aggregate.TotalXp); // Only evt2 and evt3 applied 309 Assert.Equal(2, aggregate.CurrentSequence); 310 } 311 312 [Fact] 313 public void FromEvents_ShouldThrowIfFirstEventInvalid() 314 { 315 // Arrange 316 var operatorId = OperatorId.NewId(); 317 // Create event with wrong previous hash for genesis event 318 var evt1 = new XpGainedEvent(operatorId, 0, 100, "Invalid", "should_be_empty"); 319 320 var events = new List<OperatorEvent> { evt1 }; 321 322 // Act & Assert 323 var ex = Assert.Throws<InvalidOperationException>(() => OperatorAggregate.FromEvents(events)); 324 Assert.Contains("No valid events", ex.Message); 325 } 326 327 [Fact] 328 public void NewOperator_ShouldStartWithZeroStreakAndAlive() 329 { 330 // Arrange 331 var operatorId = OperatorId.NewId(); 332 var evt = new OperatorCreatedEvent(operatorId, "TestOperator"); 333 334 // Act 335 var aggregate = OperatorAggregate.Create(evt); 336 337 // Assert 338 Assert.Equal(0, aggregate.ExfilStreak); 339 Assert.False(aggregate.IsDead); 340 } 341 }