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