33// See the LICENSE file in the project root for more information.
44
55using System . Collections . Generic ;
6+ using System . Diagnostics ;
67using System . Linq ;
78using System . Net . Test . Common ;
89using System . Threading . Tasks ;
@@ -15,6 +16,7 @@ public class HttpCookieProtocolTests : HttpClientTestBase
1516 private const string s_cookieName = "ABC" ;
1617 private const string s_cookieValue = "123" ;
1718 private const string s_expectedCookieHeaderValue = "ABC=123" ;
19+
1820 private const string s_customCookieHeaderValue = "CustomCookie=456" ;
1921
2022 private const string s_simpleContent = "Hello world!" ;
@@ -23,13 +25,18 @@ public class HttpCookieProtocolTests : HttpClientTestBase
2325 // Send cookie tests
2426 //
2527
26- private static CookieContainer CreateSingleCookieContainer ( Uri uri )
28+ private static CookieContainer CreateSingleCookieContainer ( Uri uri ) => CreateSingleCookieContainer ( uri , s_cookieName , s_cookieValue ) ;
29+
30+ private static CookieContainer CreateSingleCookieContainer ( Uri uri , string cookieName , string cookieValue )
2731 {
2832 var container = new CookieContainer ( ) ;
29- container . Add ( uri , new Cookie ( s_cookieName , s_cookieValue ) ) ;
33+ container . Add ( uri , new Cookie ( cookieName , cookieValue ) ) ;
3034 return container ;
3135 }
3236
37+ private static string GetCookieHeaderValue ( string cookieName , string cookieValue ) => $ "{ cookieName } ={ cookieValue } ";
38+
39+
3340 [ Fact ]
3441 public async Task GetAsync_DefaultCoookieContainer_NoCookieSent ( )
3542 {
@@ -48,13 +55,15 @@ await LoopbackServer.CreateServerAsync(async (server, url) =>
4855 } ) ;
4956 }
5057
51- [ Fact ]
52- public async Task GetAsync_SetCookieContainer_CookieSent ( )
58+ [ Theory ]
59+ [ MemberData ( nameof ( CookieNamesValuesAndUseCookies ) ) ]
60+ public async Task GetAsync_SetCookieContainer_CookieSent ( string cookieName , string cookieValue , bool useCookies )
5361 {
5462 await LoopbackServer . CreateServerAsync ( async ( server , url ) =>
5563 {
5664 HttpClientHandler handler = CreateHttpClientHandler ( ) ;
57- handler . CookieContainer = CreateSingleCookieContainer ( url ) ;
65+ handler . UseCookies = useCookies ;
66+ handler . CookieContainer = CreateSingleCookieContainer ( url , cookieName , cookieValue ) ;
5867
5968 using ( HttpClient client = new HttpClient ( handler ) )
6069 {
@@ -64,8 +73,15 @@ await LoopbackServer.CreateServerAsync(async (server, url) =>
6473
6574 List < string > requestLines = await serverTask ;
6675
67- Assert . Contains ( $ "Cookie: { s_expectedCookieHeaderValue } ", requestLines ) ;
68- Assert . Equal ( 1 , requestLines . Count ( s => s . StartsWith ( "Cookie:" ) ) ) ;
76+ if ( useCookies )
77+ {
78+ Assert . Contains ( $ "Cookie: { GetCookieHeaderValue ( cookieName , cookieValue ) } ", requestLines ) ;
79+ Assert . Equal ( 1 , requestLines . Count ( s => s . StartsWith ( "Cookie:" ) ) ) ;
80+ }
81+ else
82+ {
83+ Assert . Equal ( 0 , requestLines . Count ( s => s . StartsWith ( "Cookie:" ) ) ) ;
84+ }
6985 }
7086 } ) ;
7187 }
@@ -323,24 +339,34 @@ await LoopbackServer.CreateServerAndClientAsync(async url =>
323339 // Receive cookie tests
324340 //
325341
326- [ Fact ]
327- public async Task GetAsync_ReceiveSetCookieHeader_CookieAdded ( )
342+ [ Theory ]
343+ [ MemberData ( nameof ( CookieNamesValuesAndUseCookies ) ) ]
344+ public async Task GetAsync_ReceiveSetCookieHeader_CookieAdded ( string cookieName , string cookieValue , bool useCookies )
328345 {
329346 await LoopbackServer . CreateServerAsync ( async ( server , url ) =>
330347 {
331348 HttpClientHandler handler = CreateHttpClientHandler ( ) ;
349+ handler . UseCookies = useCookies ;
332350
333351 using ( HttpClient client = new HttpClient ( handler ) )
334352 {
335353 Task < HttpResponseMessage > getResponseTask = client . GetAsync ( url ) ;
336354 Task < List < string > > serverTask = LoopbackServer . ReadRequestAndSendResponseAsync ( server ,
337- $ "HTTP/1.1 200 Ok\r \n Content-Length: { s_simpleContent . Length } \r \n Set-Cookie: { s_expectedCookieHeaderValue } \r \n \r \n { s_simpleContent } ") ;
355+ $ "HTTP/1.1 200 Ok\r \n Content-Length: { s_simpleContent . Length } \r \n Set-Cookie: { GetCookieHeaderValue ( cookieName , cookieValue ) } \r \n \r \n { s_simpleContent } ") ;
338356 await TestHelper . WhenAllCompletedOrAnyFailed ( getResponseTask , serverTask ) ;
339357
340358 CookieCollection collection = handler . CookieContainer . GetCookies ( url ) ;
341- Assert . Equal ( 1 , collection . Count ) ;
342- Assert . Equal ( s_cookieName , collection [ 0 ] . Name ) ;
343- Assert . Equal ( s_cookieValue , collection [ 0 ] . Value ) ;
359+
360+ if ( useCookies )
361+ {
362+ Assert . Equal ( 1 , collection . Count ) ;
363+ Assert . Equal ( cookieName , collection [ 0 ] . Name ) ;
364+ Assert . Equal ( cookieValue , collection [ 0 ] . Value ) ;
365+ }
366+ else
367+ {
368+ Assert . Equal ( 0 , collection . Count ) ;
369+ }
344370 }
345371 } ) ;
346372 }
@@ -356,7 +382,14 @@ await LoopbackServer.CreateServerAsync(async (server, url) =>
356382 {
357383 Task < HttpResponseMessage > getResponseTask = client . GetAsync ( url ) ;
358384 Task < List < string > > serverTask = LoopbackServer . ReadRequestAndSendResponseAsync ( server ,
359- $ "HTTP/1.1 200 Ok\r \n Content-Length: { s_simpleContent . Length } \r \n Set-Cookie: A=1\r \n Set-Cookie: B=2\r \n Set-Cookie: C=3\r \n \r \n { s_simpleContent } ") ;
385+ $ "HTTP/1.1 200 OK\r \n " +
386+ $ "Date: { DateTimeOffset . UtcNow : R} \r \n " +
387+ $ "Set-Cookie: A=1; Path=/\r \n " +
388+ $ "Set-Cookie : B=2; Path=/\r \n " + // space before colon to verify header is trimmed and recognized
389+ $ "Set-Cookie: C=3; Path=/\r \n " +
390+ $ "Content-Length: { s_simpleContent . Length } \r \n " +
391+ $ "\r \n " +
392+ $ "{ s_simpleContent } ") ;
360393 await TestHelper . WhenAllCompletedOrAnyFailed ( getResponseTask , serverTask ) ;
361394
362395 CookieCollection collection = handler . CookieContainer . GetCookies ( url ) ;
@@ -512,5 +545,62 @@ await LoopbackServer.AcceptSocketAsync(server, async (_, stream, reader, writer)
512545 } ) ;
513546 } ) ;
514547 }
548+
549+ //
550+ // MemberData stuff
551+ //
552+
553+ private static string GenerateCookie ( string name , char repeat , int overallHeaderValueLength )
554+ {
555+ string emptyHeaderValue = $ "{ name } =; Path=/";
556+
557+ Debug . Assert ( overallHeaderValueLength > emptyHeaderValue . Length ) ;
558+
559+ int valueCount = overallHeaderValueLength - emptyHeaderValue . Length ;
560+ return new string ( repeat , valueCount ) ;
561+ }
562+
563+ public static IEnumerable < object [ ] > CookieNamesValuesAndUseCookies ( )
564+ {
565+ foreach ( bool useCookies in new [ ] { true , false } )
566+ {
567+ yield return new object [ ] { "ABC" , "123" , useCookies } ;
568+ yield return new object [ ] { "Hello" , "World" , useCookies } ;
569+ yield return new object [ ] { "foo" , "bar" , useCookies } ;
570+
571+ yield return new object [ ] { ".AspNetCore.Session" , "RAExEmXpoCbueP_QYM" , useCookies } ;
572+
573+ yield return new object [ ]
574+ {
575+ ".AspNetCore.Antiforgery.Xam7_OeLcN4" ,
576+ "CfDJ8NGNxAt7CbdClq3UJ8_6w_4661wRQZT1aDtUOIUKshbcV4P0NdS8klCL5qGSN-PNBBV7w23G6MYpQ81t0PMmzIN4O04fqhZ0u1YPv66mixtkX3iTi291DgwT3o5kozfQhe08-RAExEmXpoCbueP_QYM" ,
577+ useCookies
578+ } ;
579+
580+ // WinHttpHandler calls WinHttpQueryHeaders to iterate through multiple Set-Cookie header values,
581+ // using an initial buffer size of 128 chars. If the buffer is not large enough, WinHttpQueryHeaders
582+ // returns an insufficient buffer error, allowing WinHttpHandler to try again with a larger buffer.
583+ // Sometimes when WinHttpQueryHeaders fails due to insufficient buffer, it still advances the
584+ // iteration index, which would cause header values to be missed if not handled correctly.
585+ //
586+ // In particular, WinHttpQueryHeader behaves as follows for the following header value lengths:
587+ // * 0-127 chars: succeeds, index advances from 0 to 1.
588+ // * 128-255 chars: fails due to insufficient buffer, index advances from 0 to 1.
589+ // * 256+ chars: fails due to insufficient buffer, index stays at 0.
590+ //
591+ // The below overall header value lengths were chosen to exercise reading header values at these
592+ // edges, to ensure WinHttpHandler does not miss multiple Set-Cookie headers.
593+
594+ yield return new object [ ] { "foo" , GenerateCookie ( name : "foo" , repeat : 'a' , overallHeaderValueLength : 126 ) , useCookies } ;
595+ yield return new object [ ] { "foo" , GenerateCookie ( name : "foo" , repeat : 'a' , overallHeaderValueLength : 127 ) , useCookies } ;
596+ yield return new object [ ] { "foo" , GenerateCookie ( name : "foo" , repeat : 'a' , overallHeaderValueLength : 128 ) , useCookies } ;
597+ yield return new object [ ] { "foo" , GenerateCookie ( name : "foo" , repeat : 'a' , overallHeaderValueLength : 129 ) , useCookies } ;
598+
599+ yield return new object [ ] { "foo" , GenerateCookie ( name : "foo" , repeat : 'a' , overallHeaderValueLength : 254 ) , useCookies } ;
600+ yield return new object [ ] { "foo" , GenerateCookie ( name : "foo" , repeat : 'a' , overallHeaderValueLength : 255 ) , useCookies } ;
601+ yield return new object [ ] { "foo" , GenerateCookie ( name : "foo" , repeat : 'a' , overallHeaderValueLength : 256 ) , useCookies } ;
602+ yield return new object [ ] { "foo" , GenerateCookie ( name : "foo" , repeat : 'a' , overallHeaderValueLength : 257 ) , useCookies } ;
603+ }
604+ }
515605 }
516606}
0 commit comments