1+ using System ;
2+ using System . Collections . Generic ;
3+ using System . Linq ;
4+ using System . Linq . Expressions ;
5+ using System . Security . Claims ;
6+ using System . Threading ;
7+ using System . Threading . Tasks ;
8+ using AutoMapper ;
9+ using FluentAssertions ;
10+ using Microsoft . AspNetCore . Http ;
11+ using Microsoft . EntityFrameworkCore ;
12+ using Microsoft . EntityFrameworkCore . Query ;
13+ using Microsoft . Extensions . Caching . Distributed ;
14+ using Microsoft . Extensions . Logging ;
15+ using Moq ;
16+ using SorobanSecurityPortalApi . Authorization ;
17+ using SorobanSecurityPortalApi . Common ;
18+ using SorobanSecurityPortalApi . Common . Data ;
19+ using SorobanSecurityPortalApi . Data . Processors ;
20+ using SorobanSecurityPortalApi . Models . DbModels ;
21+ using SorobanSecurityPortalApi . Models . Mapping ;
22+ using SorobanSecurityPortalApi . Models . ViewModels ;
23+ using SorobanSecurityPortalApi . Services . ControllersServices ;
24+ using Xunit ;
25+
26+ namespace SorobanSecurityPortalApi . Tests . Services
27+ {
28+ public class RatingServiceTests
29+ {
30+ private readonly Mock < Db > _dbMock ;
31+ private readonly Mock < DbSet < RatingModel > > _ratingSetMock ;
32+ private readonly Mock < IDistributedCache > _cacheMock ;
33+ private readonly Mock < IHttpContextAccessor > _httpContextAccessorMock ;
34+ private readonly Mock < ILoginProcessor > _loginProcessorMock ;
35+
36+ private readonly Mock < IDbQuery > _dbQueryMock ;
37+ private readonly Mock < ILogger < Db > > _loggerMock ;
38+ private readonly Mock < IDataSourceProvider > _dataSourceProviderMock ;
39+
40+ private readonly IMapper _mapper ;
41+ private readonly RatingService _service ;
42+ private readonly List < RatingModel > _dataStore ;
43+
44+ public RatingServiceTests ( )
45+ {
46+ // Setup Data Store
47+ _dataStore = new List < RatingModel > ( ) ;
48+ _ratingSetMock = CreateDbSetMock ( _dataStore ) ;
49+
50+ // Mock Db Dependencies
51+ _dbQueryMock = new Mock < IDbQuery > ( ) ;
52+ _loggerMock = new Mock < ILogger < Db > > ( ) ;
53+ _dataSourceProviderMock = new Mock < IDataSourceProvider > ( ) ;
54+
55+ _dbMock = new Mock < Db > (
56+ _dbQueryMock . Object ,
57+ _loggerMock . Object ,
58+ _dataSourceProviderMock . Object
59+ ) { CallBase = true } ;
60+
61+ _dbMock . Object . Rating = _ratingSetMock . Object ;
62+
63+ _dbMock . Setup ( x => x . SaveChangesAsync ( It . IsAny < CancellationToken > ( ) ) ) . ReturnsAsync ( 1 ) ;
64+
65+ var mappingConfig = new MapperConfiguration ( mc => mc . AddProfile ( new RatingModelProfile ( ) ) ) ;
66+ _mapper = mappingConfig . CreateMapper ( ) ;
67+ _cacheMock = new Mock < IDistributedCache > ( ) ;
68+
69+ _httpContextAccessorMock = new Mock < IHttpContextAccessor > ( ) ;
70+ _loginProcessorMock = new Mock < ILoginProcessor > ( ) ;
71+ var userContext = new UserContextAccessor ( _httpContextAccessorMock . Object , _loginProcessorMock . Object ) ;
72+
73+ // Initialize Service
74+ _service = new RatingService ( _dbMock . Object , _cacheMock . Object , userContext , _mapper ) ;
75+ }
76+
77+ // --- TESTS ---
78+
79+ [ Fact ]
80+ public async Task AddOrUpdateRating_Should_AddToStore_WhenNew ( )
81+ {
82+ SetupLoggedInUser ( 10 ) ;
83+ var request = new CreateRatingRequest
84+ {
85+ EntityType = EntityType . Protocol ,
86+ EntityId = 1 ,
87+ Score = 5 ,
88+ Review = "Fresh Review"
89+ } ;
90+
91+ await _service . AddOrUpdateRating ( request ) ;
92+
93+ _dataStore . Should ( ) . HaveCount ( 1 ) ;
94+ _dataStore [ 0 ] . Review . Should ( ) . Be ( "Fresh Review" ) ;
95+ }
96+
97+ [ Fact ]
98+ public async Task AddOrUpdateRating_Should_UpdateStore_WhenExists ( )
99+ {
100+ SetupLoggedInUser ( 10 ) ;
101+ _dataStore . Add ( new RatingModel { UserId = 10 , EntityId = 1 , EntityType = EntityType . Protocol , Score = 1 , Review = "Old" } ) ;
102+
103+ var request = new CreateRatingRequest
104+ {
105+ EntityType = EntityType . Protocol ,
106+ EntityId = 1 ,
107+ Score = 5 ,
108+ Review = "Updated"
109+ } ;
110+
111+ await _service . AddOrUpdateRating ( request ) ;
112+
113+ _dataStore . Should ( ) . HaveCount ( 1 ) ;
114+ _dataStore [ 0 ] . Review . Should ( ) . Be ( "Updated" ) ;
115+ }
116+
117+ [ Fact ]
118+ public async Task GetSummary_Should_Calculate_FromStore ( )
119+ {
120+ _dataStore . Add ( new RatingModel { UserId = 1 , EntityId = 100 , EntityType = EntityType . Protocol , Score = 5 } ) ;
121+ _dataStore . Add ( new RatingModel { UserId = 2 , EntityId = 100 , EntityType = EntityType . Protocol , Score = 5 } ) ;
122+ _dataStore . Add ( new RatingModel { UserId = 3 , EntityId = 100 , EntityType = EntityType . Protocol , Score = 1 } ) ;
123+
124+ var summary = await _service . GetSummary ( EntityType . Protocol , 100 ) ;
125+
126+ summary . TotalReviews . Should ( ) . Be ( 3 ) ;
127+ summary . AverageScore . Should ( ) . Be ( 3.7f ) ;
128+ }
129+
130+ [ Fact ]
131+ public async Task DeleteRating_Should_RemoveFromStore_WhenOwner ( )
132+ {
133+ int userId = 50 ;
134+ SetupLoggedInUser ( userId ) ;
135+ var rating = new RatingModel { Id = 1 , UserId = userId , EntityId = 1 , EntityType = EntityType . Protocol } ;
136+ _dataStore . Add ( rating ) ;
137+
138+ _ratingSetMock . Setup ( x => x . FindAsync ( 1 ) ) . ReturnsAsync ( rating ) ;
139+
140+ await _service . DeleteRating ( 1 ) ;
141+
142+ _ratingSetMock . Verify ( x => x . Remove ( rating ) , Times . Once ) ;
143+ }
144+
145+ [ Fact ]
146+ public async Task DeleteRating_Should_Throw_If_Hacker ( )
147+ {
148+ SetupLoggedInUser ( 99 , isAdmin : false ) ;
149+ var rating = new RatingModel { Id = 1 , UserId = 50 , EntityId = 1 , EntityType = EntityType . Protocol } ;
150+ _dataStore . Add ( rating ) ;
151+ _ratingSetMock . Setup ( x => x . FindAsync ( 1 ) ) . ReturnsAsync ( rating ) ;
152+
153+ await Assert . ThrowsAsync < UnauthorizedAccessException > ( ( ) => _service . DeleteRating ( 1 ) ) ;
154+ }
155+
156+ [ Fact ]
157+ public async Task DeleteRating_Should_Allow_Admin ( )
158+ {
159+ SetupLoggedInUser ( 999 , isAdmin : true ) ;
160+ var rating = new RatingModel { Id = 1 , UserId = 50 , EntityId = 1 , EntityType = EntityType . Protocol } ;
161+ _dataStore . Add ( rating ) ;
162+ _ratingSetMock . Setup ( x => x . FindAsync ( 1 ) ) . ReturnsAsync ( rating ) ;
163+
164+ await _service . DeleteRating ( 1 ) ;
165+ _ratingSetMock . Verify ( x => x . Remove ( rating ) , Times . Once ) ;
166+ }
167+
168+ [ Fact ]
169+ public async Task GetSummary_Should_ReturnZeroes_WhenNoRatingsExist ( )
170+ {
171+ var summary = await _service . GetSummary ( EntityType . Protocol , 999 ) ;
172+ summary . TotalReviews . Should ( ) . Be ( 0 ) ;
173+ }
174+
175+ [ Fact ]
176+ public async Task GetRatings_Should_Paginate_Correctly ( )
177+ {
178+ for ( int i = 1 ; i <= 15 ; i ++ )
179+ {
180+ _dataStore . Add ( new RatingModel
181+ {
182+ Id = i , UserId = i , EntityId = 50 , EntityType = EntityType . Protocol , Score = 5 ,
183+ CreatedAt = DateTime . UtcNow . AddMinutes ( i )
184+ } ) ;
185+ }
186+
187+ var result = await _service . GetRatings ( EntityType . Protocol , 50 , page : 2 , pageSize : 10 ) ;
188+ result . Should ( ) . HaveCount ( 5 ) ;
189+ result . First ( ) . Id . Should ( ) . Be ( 5 ) ;
190+ }
191+
192+ // --- HELPERS ---
193+
194+ private void SetupLoggedInUser ( int userId , bool isAdmin = false )
195+ {
196+ var claims = new List < Claim > { new Claim ( ClaimTypes . NameIdentifier , "user@test.com" ) } ;
197+ var identity = new ClaimsIdentity ( claims , "Test" ) ;
198+ var principal = new ClaimsPrincipal ( identity ) ;
199+
200+ _httpContextAccessorMock . Setup ( x => x . HttpContext ) . Returns ( new DefaultHttpContext { User = principal } ) ;
201+
202+ var loginModel = new LoginModel { LoginId = userId , Login = "user@test.com" , Role = isAdmin ? RoleEnum . Admin : RoleEnum . User } ;
203+ _loginProcessorMock . Setup ( x => x . GetByLogin ( It . IsAny < string > ( ) , It . IsAny < LoginTypeEnum > ( ) ) ) . ReturnsAsync ( loginModel ) ;
204+ _loginProcessorMock . Setup ( x => x . GetById ( userId ) ) . ReturnsAsync ( loginModel ) ;
205+ }
206+
207+ private static Mock < DbSet < T > > CreateDbSetMock < T > ( List < T > sourceList ) where T : class
208+ {
209+ var queryable = sourceList . AsQueryable ( ) ;
210+ var dbSetMock = new Mock < DbSet < T > > ( ) ;
211+
212+ dbSetMock . As < IQueryable < T > > ( ) . Setup ( m => m . Provider ) . Returns ( new TestAsyncQueryProvider < T > ( queryable . Provider ) ) ;
213+ dbSetMock . As < IQueryable < T > > ( ) . Setup ( m => m . Expression ) . Returns ( queryable . Expression ) ;
214+ dbSetMock . As < IQueryable < T > > ( ) . Setup ( m => m . ElementType ) . Returns ( queryable . ElementType ) ;
215+ dbSetMock . As < IQueryable < T > > ( ) . Setup ( m => m . GetEnumerator ( ) ) . Returns ( queryable . GetEnumerator ( ) ) ;
216+ dbSetMock . As < IAsyncEnumerable < T > > ( ) . Setup ( m => m . GetAsyncEnumerator ( It . IsAny < CancellationToken > ( ) ) )
217+ . Returns ( new TestAsyncEnumerator < T > ( queryable . GetEnumerator ( ) ) ) ;
218+
219+ dbSetMock . Setup ( d => d . Add ( It . IsAny < T > ( ) ) ) . Callback < T > ( sourceList . Add ) ;
220+ dbSetMock . Setup ( d => d . AddRange ( It . IsAny < IEnumerable < T > > ( ) ) ) . Callback < IEnumerable < T > > ( sourceList . AddRange ) ;
221+
222+ return dbSetMock ;
223+ }
224+ }
225+
226+ // --- INFRASTRUCTURE ---
227+
228+ internal class TestAsyncQueryProvider < TEntity > : IAsyncQueryProvider
229+ {
230+ private readonly IQueryProvider _inner ;
231+ internal TestAsyncQueryProvider ( IQueryProvider inner ) => _inner = inner ;
232+ public IQueryable CreateQuery ( Expression expression ) => new TestAsyncEnumerable < TEntity > ( expression ) ;
233+ public IQueryable < TElement > CreateQuery < TElement > ( Expression expression ) => new TestAsyncEnumerable < TElement > ( expression ) ;
234+ public object ? Execute ( Expression expression ) => _inner . Execute ( expression ) ;
235+ public TResult Execute < TResult > ( Expression expression ) => _inner . Execute < TResult > ( expression ) ;
236+ public TResult ExecuteAsync < TResult > ( Expression expression , CancellationToken cancellationToken )
237+ {
238+ var expectedResultType = typeof ( TResult ) . GetGenericArguments ( ) [ 0 ] ;
239+ var executionResult = typeof ( IQueryProvider )
240+ . GetMethod ( nameof ( IQueryProvider . Execute ) , 1 , new [ ] { typeof ( Expression ) } ) !
241+ . MakeGenericMethod ( expectedResultType )
242+ . Invoke ( this , new [ ] { expression } ) ;
243+ return ( TResult ) typeof ( Task ) . GetMethod ( nameof ( Task . FromResult ) ) !
244+ . MakeGenericMethod ( expectedResultType )
245+ . Invoke ( null , new [ ] { executionResult } ) ! ;
246+ }
247+ }
248+
249+ internal class TestAsyncEnumerable < T > : EnumerableQuery < T > , IAsyncEnumerable < T > , IQueryable < T >
250+ {
251+ public TestAsyncEnumerable ( IEnumerable < T > enumerable ) : base ( enumerable ) { }
252+ public TestAsyncEnumerable ( Expression expression ) : base ( expression ) { }
253+ public IAsyncEnumerator < T > GetAsyncEnumerator ( CancellationToken cancellationToken = default ) =>
254+ new TestAsyncEnumerator < T > ( this . AsEnumerable ( ) . GetEnumerator ( ) ) ;
255+ IQueryProvider IQueryable . Provider => new TestAsyncQueryProvider < T > ( this ) ;
256+ }
257+
258+ internal class TestAsyncEnumerator < T > : IAsyncEnumerator < T >
259+ {
260+ private readonly IEnumerator < T > _inner ;
261+ public TestAsyncEnumerator ( IEnumerator < T > inner ) => _inner = inner ;
262+ public ValueTask DisposeAsync ( ) { _inner . Dispose ( ) ; return ValueTask . CompletedTask ; }
263+ public ValueTask < bool > MoveNextAsync ( ) => ValueTask . FromResult ( _inner . MoveNext ( ) ) ;
264+ public T Current => _inner . Current ;
265+ }
266+ }
0 commit comments