1+ using FluentAssertions ;
2+ using Microsoft . AspNetCore . Http ;
3+ using Microsoft . AspNetCore . Mvc ;
4+ using Microsoft . AspNetCore . Mvc . Abstractions ;
5+ using Microsoft . AspNetCore . Mvc . Filters ;
6+ using Microsoft . AspNetCore . Routing ;
7+ using Microsoft . Extensions . DependencyInjection ;
8+ using Microsoft . Extensions . Logging ;
9+ using Moq ;
10+ using ProjectVG . Api . Filters ;
11+ using ProjectVG . Common . Constants ;
12+ using ProjectVG . Common . Exceptions ;
13+ using ProjectVG . Infrastructure . Auth ;
14+ using System . Security . Claims ;
15+ using Xunit ;
16+
17+ namespace ProjectVG . Tests . Api . Filters
18+ {
19+ public class JwtAuthenticationFilterTests
20+ {
21+ private readonly Mock < ITokenService > _mockTokenService ;
22+ private readonly Mock < ILogger < JwtAuthenticationAttribute > > _mockLogger ;
23+ private readonly JwtAuthenticationAttribute _filter ;
24+ private readonly ServiceProvider _serviceProvider ;
25+
26+ public JwtAuthenticationFilterTests ( )
27+ {
28+ _mockTokenService = new Mock < ITokenService > ( ) ;
29+ _mockLogger = new Mock < ILogger < JwtAuthenticationAttribute > > ( ) ;
30+
31+ var services = new ServiceCollection ( ) ;
32+ services . AddSingleton ( _mockTokenService . Object ) ;
33+ services . AddSingleton ( _mockLogger . Object ) ;
34+ _serviceProvider = services . BuildServiceProvider ( ) ;
35+
36+ _filter = new JwtAuthenticationAttribute ( ) ;
37+ }
38+
39+ private AuthorizationFilterContext CreateFilterContext ( string ? headerName = null , string ? headerValue = null )
40+ {
41+ var httpContext = new DefaultHttpContext
42+ {
43+ RequestServices = _serviceProvider
44+ } ;
45+
46+ if ( ! string . IsNullOrEmpty ( headerName ) && ! string . IsNullOrEmpty ( headerValue ) )
47+ {
48+ httpContext . Request . Headers [ headerName ] = headerValue ;
49+ }
50+
51+ var actionContext = new ActionContext (
52+ httpContext ,
53+ new RouteData ( ) ,
54+ new ActionDescriptor ( )
55+ ) ;
56+
57+ return new AuthorizationFilterContext ( actionContext , new List < IFilterMetadata > ( ) ) ;
58+ }
59+
60+ #region Token Extraction Tests
61+
62+ [ Theory ]
63+ [ InlineData ( "Authorization" , "Bearer valid-token-123" ) ]
64+ [ InlineData ( "X-Forwarded-Authorization" , "Bearer forwarded-token-456" ) ]
65+ [ InlineData ( "X-Original-Authorization" , "Bearer original-token-789" ) ]
66+ [ InlineData ( "HTTP_AUTHORIZATION" , "Bearer http-token-abc" ) ]
67+ public async Task OnAuthorizationAsync_ValidTokenInDifferentHeaders_ShouldAuthenticate ( string headerName , string headerValue )
68+ {
69+ // Arrange
70+ var userId = Guid . NewGuid ( ) ;
71+ var expectedToken = headerValue . Substring ( "Bearer " . Length ) ;
72+ var filterContext = CreateFilterContext ( headerName , headerValue ) ;
73+
74+ _mockTokenService . Setup ( x => x . ValidateAccessTokenAsync ( expectedToken ) )
75+ . ReturnsAsync ( true ) ;
76+ _mockTokenService . Setup ( x => x . GetUserIdFromTokenAsync ( expectedToken ) )
77+ . ReturnsAsync ( userId ) ;
78+
79+ // Act
80+ await _filter . OnAuthorizationAsync ( filterContext ) ;
81+
82+ // Assert
83+ filterContext . HttpContext . User . Should ( ) . NotBeNull ( ) ;
84+ filterContext . HttpContext . User . Identity ! . IsAuthenticated . Should ( ) . BeTrue ( ) ;
85+ filterContext . HttpContext . User . Identity . AuthenticationType . Should ( ) . Be ( "Bearer" ) ;
86+
87+ var userIdClaim = filterContext . HttpContext . User . FindFirst ( ClaimTypes . NameIdentifier ) ;
88+ userIdClaim . Should ( ) . NotBeNull ( ) ;
89+ userIdClaim ! . Value . Should ( ) . Be ( userId . ToString ( ) ) ;
90+
91+ var customUserIdClaim = filterContext . HttpContext . User . FindFirst ( "user_id" ) ;
92+ customUserIdClaim . Should ( ) . NotBeNull ( ) ;
93+ customUserIdClaim ! . Value . Should ( ) . Be ( userId . ToString ( ) ) ;
94+ }
95+
96+ [ Fact ]
97+ public async Task OnAuthorizationAsync_TokenWithExtraSpaces_ShouldTrimAndUse ( )
98+ {
99+ // Arrange
100+ var userId = Guid . NewGuid ( ) ;
101+ var token = "token-with-spaces" ;
102+ var filterContext = CreateFilterContext ( "Authorization" , $ "Bearer { token } ") ;
103+
104+ _mockTokenService . Setup ( x => x . ValidateAccessTokenAsync ( token ) )
105+ . ReturnsAsync ( true ) ;
106+ _mockTokenService . Setup ( x => x . GetUserIdFromTokenAsync ( token ) )
107+ . ReturnsAsync ( userId ) ;
108+
109+ // Act
110+ await _filter . OnAuthorizationAsync ( filterContext ) ;
111+
112+ // Assert
113+ _mockTokenService . Verify ( x => x . ValidateAccessTokenAsync ( token ) , Times . Once ) ;
114+ filterContext . HttpContext . User . Identity ! . IsAuthenticated . Should ( ) . BeTrue ( ) ;
115+ }
116+
117+ #endregion
118+
119+ #region Token Missing Tests
120+
121+ [ Fact ]
122+ public async Task OnAuthorizationAsync_NoAuthorizationHeader_ShouldThrowTokenMissingException ( )
123+ {
124+ // Arrange
125+ var filterContext = CreateFilterContext ( ) ;
126+
127+ // Act & Assert
128+ var exception = await Assert . ThrowsAsync < AuthenticationException > (
129+ ( ) => _filter . OnAuthorizationAsync ( filterContext )
130+ ) ;
131+
132+ exception . ErrorCode . Should ( ) . Be ( ErrorCode . TOKEN_MISSING ) ;
133+ }
134+
135+ [ Fact ]
136+ public async Task OnAuthorizationAsync_EmptyAuthorizationHeader_ShouldThrowTokenMissingException ( )
137+ {
138+ // Arrange
139+ var filterContext = CreateFilterContext ( "Authorization" , "" ) ;
140+
141+ // Act & Assert
142+ var exception = await Assert . ThrowsAsync < AuthenticationException > (
143+ ( ) => _filter . OnAuthorizationAsync ( filterContext )
144+ ) ;
145+
146+ exception . ErrorCode . Should ( ) . Be ( ErrorCode . TOKEN_MISSING ) ;
147+ }
148+
149+ [ Fact ]
150+ public async Task OnAuthorizationAsync_BearerWithoutToken_ShouldThrowTokenMissingException ( )
151+ {
152+ // Arrange
153+ var filterContext = CreateFilterContext ( "Authorization" , "Bearer" ) ;
154+
155+ // Act & Assert
156+ var exception = await Assert . ThrowsAsync < AuthenticationException > (
157+ ( ) => _filter . OnAuthorizationAsync ( filterContext )
158+ ) ;
159+
160+ exception . ErrorCode . Should ( ) . Be ( ErrorCode . TOKEN_MISSING ) ;
161+ }
162+
163+ [ Fact ]
164+ public async Task OnAuthorizationAsync_BearerWithEmptyToken_ShouldThrowTokenMissingException ( )
165+ {
166+ // Arrange
167+ var filterContext = CreateFilterContext ( "Authorization" , "Bearer " ) ;
168+
169+ // Act & Assert
170+ var exception = await Assert . ThrowsAsync < AuthenticationException > (
171+ ( ) => _filter . OnAuthorizationAsync ( filterContext )
172+ ) ;
173+
174+ exception . ErrorCode . Should ( ) . Be ( ErrorCode . TOKEN_MISSING ) ;
175+ }
176+
177+ #endregion
178+
179+ #region Token Validation Tests
180+
181+ [ Fact ]
182+ public async Task OnAuthorizationAsync_InvalidToken_ShouldThrowTokenInvalidException ( )
183+ {
184+ // Arrange
185+ var invalidToken = "invalid-token" ;
186+ var filterContext = CreateFilterContext ( "Authorization" , $ "Bearer { invalidToken } ") ;
187+
188+ _mockTokenService . Setup ( x => x . ValidateAccessTokenAsync ( invalidToken ) )
189+ . ReturnsAsync ( false ) ;
190+
191+ // Act & Assert
192+ var exception = await Assert . ThrowsAsync < AuthenticationException > (
193+ ( ) => _filter . OnAuthorizationAsync ( filterContext )
194+ ) ;
195+
196+ exception . ErrorCode . Should ( ) . Be ( ErrorCode . TOKEN_INVALID ) ;
197+ }
198+
199+ #endregion
200+
201+ #region User ID Extraction Tests
202+
203+ [ Fact ]
204+ public async Task OnAuthorizationAsync_ValidTokenButNoUserId_ShouldThrowAuthenticationFailedException ( )
205+ {
206+ // Arrange
207+ var validToken = "valid-token-no-user" ;
208+ var filterContext = CreateFilterContext ( "Authorization" , $ "Bearer { validToken } ") ;
209+
210+ _mockTokenService . Setup ( x => x . ValidateAccessTokenAsync ( validToken ) )
211+ . ReturnsAsync ( true ) ;
212+ _mockTokenService . Setup ( x => x . GetUserIdFromTokenAsync ( validToken ) )
213+ . ReturnsAsync ( ( Guid ? ) null ) ;
214+
215+ // Act & Assert
216+ var exception = await Assert . ThrowsAsync < AuthenticationException > (
217+ ( ) => _filter . OnAuthorizationAsync ( filterContext )
218+ ) ;
219+
220+ exception . ErrorCode . Should ( ) . Be ( ErrorCode . AUTHENTICATION_FAILED ) ;
221+ }
222+
223+ #endregion
224+
225+ #region Successful Authentication Tests
226+
227+ [ Fact ]
228+ public async Task OnAuthorizationAsync_ValidTokenAndUserId_ShouldSetUserWithCorrectClaims ( )
229+ {
230+ // Arrange
231+ var userId = Guid . NewGuid ( ) ;
232+ var validToken = "valid-token-123" ;
233+ var filterContext = CreateFilterContext ( "Authorization" , $ "Bearer { validToken } ") ;
234+
235+ _mockTokenService . Setup ( x => x . ValidateAccessTokenAsync ( validToken ) )
236+ . ReturnsAsync ( true ) ;
237+ _mockTokenService . Setup ( x => x . GetUserIdFromTokenAsync ( validToken ) )
238+ . ReturnsAsync ( userId ) ;
239+
240+ // Act
241+ await _filter . OnAuthorizationAsync ( filterContext ) ;
242+
243+ // Assert
244+ var user = filterContext . HttpContext . User ;
245+ user . Should ( ) . NotBeNull ( ) ;
246+ user . Identity ! . IsAuthenticated . Should ( ) . BeTrue ( ) ;
247+ user . Identity . AuthenticationType . Should ( ) . Be ( "Bearer" ) ;
248+
249+ var nameIdentifierClaim = user . FindFirst ( ClaimTypes . NameIdentifier ) ;
250+ nameIdentifierClaim . Should ( ) . NotBeNull ( ) ;
251+ nameIdentifierClaim ! . Value . Should ( ) . Be ( userId . ToString ( ) ) ;
252+
253+ var userIdClaim = user . FindFirst ( "user_id" ) ;
254+ userIdClaim . Should ( ) . NotBeNull ( ) ;
255+ userIdClaim ! . Value . Should ( ) . Be ( userId . ToString ( ) ) ;
256+
257+ user . Claims . Should ( ) . HaveCount ( 2 ) ;
258+ }
259+
260+ [ Fact ]
261+ public async Task OnAuthorizationAsync_CompleteValidFlow_ShouldNotSetResult ( )
262+ {
263+ // Arrange
264+ var userId = Guid . NewGuid ( ) ;
265+ var validToken = "complete-valid-token" ;
266+ var filterContext = CreateFilterContext ( "Authorization" , $ "Bearer { validToken } ") ;
267+
268+ _mockTokenService . Setup ( x => x . ValidateAccessTokenAsync ( validToken ) )
269+ . ReturnsAsync ( true ) ;
270+ _mockTokenService . Setup ( x => x . GetUserIdFromTokenAsync ( validToken ) )
271+ . ReturnsAsync ( userId ) ;
272+
273+ // Act
274+ await _filter . OnAuthorizationAsync ( filterContext ) ;
275+
276+ // Assert
277+ filterContext . Result . Should ( ) . BeNull ( ) ; // No result means continue with request
278+ filterContext . HttpContext . User . Identity ! . IsAuthenticated . Should ( ) . BeTrue ( ) ;
279+ }
280+
281+ #endregion
282+ }
283+ }
0 commit comments