1+ using EssentialCSharp . Web . Models ;
12using EssentialCSharp . Web . Services ;
23using Microsoft . AspNetCore . Mvc . Testing ;
34using Microsoft . Extensions . DependencyInjection ;
@@ -8,13 +9,41 @@ namespace EssentialCSharp.Web.Tests;
89[ ClassDataSource < WebApplicationFactory > ( Shared = SharedType . PerClass ) ]
910public class McpApiTokenServiceTests ( WebApplicationFactory factory )
1011{
11- [ Test ]
12- public async Task CreateTokenAsync_WithoutExpiry_UsesSixMonthDefault ( )
12+ private readonly List < IServiceScope > _scopes = [ ] ;
13+
14+ [ After ( Test ) ]
15+ public void DisposeScopes ( )
1316 {
14- string userId = await McpTestHelper . CreateUserAsync ( factory , "mcp-default-expiry" ) ;
17+ foreach ( var scope in _scopes )
18+ scope . Dispose ( ) ;
19+ _scopes . Clear ( ) ;
20+ }
21+
22+ private async Task < ( string UserId , McpApiTokenService TokenService ) > ArrangeAsync ( string prefix )
23+ {
24+ string userId = await McpTestHelper . CreateUserAsync ( factory , prefix ) ;
25+ var scope = factory . Services . CreateScope ( ) ;
26+ _scopes . Add ( scope ) ;
27+ var tokenService = scope . ServiceProvider . GetRequiredService < McpApiTokenService > ( ) ;
28+ return ( userId , tokenService ) ;
29+ }
1530
16- using var scope = factory . Services . CreateScope ( ) ;
31+ private async Task < McpApiTokenService > FillToLimitAsync ( string userId )
32+ {
33+ var scope = factory . Services . CreateScope ( ) ;
34+ _scopes . Add ( scope ) ;
1735 var tokenService = scope . ServiceProvider . GetRequiredService < McpApiTokenService > ( ) ;
36+ for ( int i = 0 ; i < McpApiTokenService . MaxTokensPerUser ; i ++ )
37+ {
38+ await tokenService . CreateTokenAsync ( userId , $ "token-{ i } ") ;
39+ }
40+ return tokenService ;
41+ }
42+
43+ [ Test ]
44+ public async Task CreateTokenAsync_WithoutExpiry_UsesSixMonthDefault ( )
45+ {
46+ var ( userId , tokenService ) = await ArrangeAsync ( "mcp-default-expiry" ) ;
1847
1948 ( _ , var entity ) = await tokenService . CreateTokenAsync ( userId , "default-expiry" ) ;
2049
@@ -26,10 +55,7 @@ await Assert.That(entity.ExpiresAt!.Value)
2655 [ Test ]
2756 public async Task CreateTokenAsync_WithExpiryWithinSixMonths_UsesRequestedExpiry ( )
2857 {
29- string userId = await McpTestHelper . CreateUserAsync ( factory , "mcp-custom-expiry" ) ;
30-
31- using var scope = factory . Services . CreateScope ( ) ;
32- var tokenService = scope . ServiceProvider . GetRequiredService < McpApiTokenService > ( ) ;
58+ var ( userId , tokenService ) = await ArrangeAsync ( "mcp-custom-expiry" ) ;
3359 DateTime requestedExpiry = DateTime . UtcNow . AddMonths ( 3 ) ;
3460
3561 ( _ , var entity ) = await tokenService . CreateTokenAsync ( userId , "custom-expiry" , requestedExpiry ) ;
@@ -41,10 +67,7 @@ public async Task CreateTokenAsync_WithExpiryWithinSixMonths_UsesRequestedExpiry
4167 [ Test ]
4268 public async Task CreateTokenAsync_WithExpiryBeyondSixMonths_Throws ( )
4369 {
44- string userId = await McpTestHelper . CreateUserAsync ( factory , "mcp-max-expiry" ) ;
45-
46- using var scope = factory . Services . CreateScope ( ) ;
47- var tokenService = scope . ServiceProvider . GetRequiredService < McpApiTokenService > ( ) ;
70+ var ( userId , tokenService ) = await ArrangeAsync ( "mcp-max-expiry" ) ;
4871 DateTime requestedExpiry = McpApiTokenService . GetDefaultExpirationUtc ( DateTime . UtcNow ) . AddDays ( 2 ) ;
4972
5073 await Assert . That ( ( ) => tokenService . CreateTokenAsync ( userId , "too-long" , requestedExpiry ) )
@@ -55,20 +78,97 @@ await Assert.That(() => tokenService.CreateTokenAsync(userId, "too-long", reques
5578 [ Test ]
5679 public async Task CreateTokenAsync_WithExplicitCreatedAt_UsesReferenceTimeForDefaultExpiry ( )
5780 {
58- string userId = await McpTestHelper . CreateUserAsync ( factory , "mcp-explicit-created-at" ) ;
59-
60- using var scope = factory . Services . CreateScope ( ) ;
61- var tokenService = scope . ServiceProvider . GetRequiredService < McpApiTokenService > ( ) ;
81+ var ( userId , tokenService ) = await ArrangeAsync ( "mcp-explicit-created-at" ) ;
6282 DateTime createdAtUtc = new ( 2026 , 4 , 30 , 23 , 59 , 59 , DateTimeKind . Utc ) ;
6383
6484 ( _ , var entity ) = await tokenService . CreateTokenAsync (
65- userId ,
66- "explicit-created-at" ,
67- createdAtUtc : createdAtUtc ) ;
85+ userId , "explicit-created-at" , createdAtUtc : createdAtUtc ) ;
6886
6987 await Assert . That ( entity . CreatedAt ) . IsEqualTo ( createdAtUtc ) ;
7088 await Assert . That ( entity . ExpiresAt ) . IsNotNull ( ) ;
7189 await Assert . That ( entity . ExpiresAt ! . Value )
7290 . IsEqualTo ( McpApiTokenService . GetDefaultExpirationUtc ( createdAtUtc ) ) ;
7391 }
92+
93+ [ Test ]
94+ public async Task GetActiveTokenCountAsync_NoTokens_ReturnsZero ( )
95+ {
96+ var ( userId , tokenService ) = await ArrangeAsync ( "mcp-count-zero" ) ;
97+
98+ int count = await tokenService . GetActiveTokenCountAsync ( userId ) ;
99+
100+ await Assert . That ( count ) . IsEqualTo ( 0 ) ;
101+ }
102+
103+ [ Test ]
104+ public async Task GetActiveTokenCountAsync_ActiveTokens_CountsAll ( )
105+ {
106+ var ( userId , tokenService ) = await ArrangeAsync ( "mcp-count-active" ) ;
107+
108+ await tokenService . CreateTokenAsync ( userId , "token-1" ) ;
109+ await tokenService . CreateTokenAsync ( userId , "token-2" ) ;
110+ await tokenService . CreateTokenAsync ( userId , "token-3" ) ;
111+
112+ int count = await tokenService . GetActiveTokenCountAsync ( userId ) ;
113+
114+ await Assert . That ( count ) . IsEqualTo ( 3 ) ;
115+ }
116+
117+ [ Test ]
118+ public async Task GetActiveTokenCountAsync_RevokedToken_ExcludedFromCount ( )
119+ {
120+ var ( userId , tokenService ) = await ArrangeAsync ( "mcp-count-revoked" ) ;
121+
122+ await tokenService . CreateTokenAsync ( userId , "active-token" ) ;
123+ ( _ , var revokedEntity ) = await tokenService . CreateTokenAsync ( userId , "revoked-token" ) ;
124+ await tokenService . RevokeTokenAsync ( revokedEntity . Id , userId ) ;
125+
126+ int count = await tokenService . GetActiveTokenCountAsync ( userId ) ;
127+
128+ await Assert . That ( count ) . IsEqualTo ( 1 ) ;
129+ }
130+
131+ [ Test ]
132+ public async Task GetActiveTokenCountAsync_ExpiredToken_ExcludedFromCount ( )
133+ {
134+ var ( userId , tokenService ) = await ArrangeAsync ( "mcp-count-expired" ) ;
135+
136+ // createdAt 7 months ago → max expiry = 1 month ago; use 2 months ago as expiresAt
137+ DateTime createdAt = DateTime . UtcNow . AddMonths ( - 7 ) ;
138+ DateTime pastExpiry = DateTime . UtcNow . AddMonths ( - 2 ) ;
139+ await tokenService . CreateTokenAsync ( userId , "expired-token" ,
140+ expiresAt : pastExpiry , createdAtUtc : createdAt ) ;
141+ await tokenService . CreateTokenAsync ( userId , "valid-token" ) ;
142+
143+ int count = await tokenService . GetActiveTokenCountAsync ( userId ) ;
144+
145+ await Assert . That ( count ) . IsEqualTo ( 1 ) ;
146+ }
147+
148+ [ Test ]
149+ public async Task CreateTokenAsync_AtMaxLimit_ThrowsTokenLimitExceededException ( )
150+ {
151+ var ( userId , _) = await ArrangeAsync ( "mcp-at-limit" ) ;
152+ var tokenService = await FillToLimitAsync ( userId ) ;
153+
154+ await Assert . That ( ( ) => tokenService . CreateTokenAsync ( userId , "one-too-many" ) )
155+ . Throws < TokenLimitExceededException > ( ) ;
156+ }
157+
158+ [ Test ]
159+ public async Task CreateTokenAsync_AfterRevokingAtLimit_AllowsNewToken ( )
160+ {
161+ var ( userId , _) = await ArrangeAsync ( "mcp-revoke-then-create" ) ;
162+ var tokenService = await FillToLimitAsync ( userId ) ;
163+
164+ // Revoke the last token to free a slot
165+ var tokens = await tokenService . GetUserTokensAsync ( userId ) ;
166+ await tokenService . RevokeTokenAsync ( tokens [ 0 ] . Id , userId ) ;
167+
168+ // Should now succeed — active count dropped below max
169+ ( _ , var newEntity ) = await tokenService . CreateTokenAsync ( userId , "replacement" ) ;
170+ await Assert . That ( newEntity ) . IsNotNull ( ) ;
171+ int activeCount = await tokenService . GetActiveTokenCountAsync ( userId ) ;
172+ await Assert . That ( activeCount ) . IsEqualTo ( McpApiTokenService . MaxTokensPerUser ) ;
173+ }
74174}
0 commit comments