@@ -277,6 +277,179 @@ public async Task CrashPostClient_PostCrashFile_AllowAdditionalFormDataAttachmen
277277 Assert . AreEqual ( expectedContent , attachmentContent ) ;
278278 }
279279
280+ [ Test ]
281+ public async Task CrashPostClient_PostFeedback_ShouldReturn200 ( )
282+ {
283+ var bugsplat = new BugSplat ( database , application , version ) ;
284+ var getCrashUrl = "https://fake.url.com" ;
285+ var mockHttp = CreateMockHttpClientForExceptionPost ( getCrashUrl ) ;
286+ var httpClient = new HttpClient ( mockHttp . Object ) ;
287+ var httpClientFactory = new FakeHttpClientFactory ( httpClient ) ;
288+ var mockS3ClientFactory = FakeS3ClientFactory . CreateMockS3ClientFactory ( ) ;
289+
290+ var sut = new CrashPostClient ( httpClientFactory , mockS3ClientFactory ) ;
291+
292+ var postResult = await sut . PostFeedback (
293+ database ,
294+ application ,
295+ version ,
296+ "Test feedback title" ,
297+ FeedbackPostOptions . Create ( bugsplat )
298+ ) ;
299+
300+ Assert . AreEqual ( HttpStatusCode . OK , postResult . StatusCode ) ;
301+ }
302+
303+ [ Test ]
304+ public async Task CrashPostClient_PostFeedback_ShouldUseCrashTypeId36 ( )
305+ {
306+ var bugsplat = new BugSplat ( database , application , version ) ;
307+ var getCrashUrl = "https://fake.url.com" ;
308+ HttpRequestMessage capturedCommitRequest = null ;
309+ var mockHttp = CreateMockHttpClientWithCapture ( getCrashUrl , req => capturedCommitRequest = req ) ;
310+ var httpClient = new HttpClient ( mockHttp . Object ) ;
311+ var httpClientFactory = new FakeHttpClientFactory ( httpClient ) ;
312+ var mockS3ClientFactory = FakeS3ClientFactory . CreateMockS3ClientFactory ( ) ;
313+
314+ var sut = new CrashPostClient ( httpClientFactory , mockS3ClientFactory ) ;
315+
316+ await sut . PostFeedback (
317+ database ,
318+ application ,
319+ version ,
320+ "Test feedback title" ,
321+ FeedbackPostOptions . Create ( bugsplat )
322+ ) ;
323+
324+ Assert . IsNotNull ( capturedCommitRequest ) ;
325+ var content = capturedCommitRequest . Content as MultipartFormDataContent ;
326+ Assert . IsNotNull ( content ) ;
327+ var formData = await content . ReadAsStringAsync ( ) ;
328+ Assert . That ( formData , Does . Contain ( "36" ) ) ;
329+ }
330+
331+ [ Test ]
332+ public async Task CrashPostClient_PostFeedback_ShouldConstructFeedbackJsonWithEscaping ( )
333+ {
334+ var bugsplat = new BugSplat ( database , application , version ) ;
335+ bugsplat . Description = "Line1\n Line2\t with \" quotes\" " ;
336+ var getCrashUrl = "https://fake.url.com" ;
337+ var mockHttp = CreateMockHttpClientForExceptionPost ( getCrashUrl ) ;
338+ var httpClient = new HttpClient ( mockHttp . Object ) ;
339+ var httpClientFactory = new FakeHttpClientFactory ( httpClient ) ;
340+ var mockS3ClientFactory = FakeS3ClientFactory . CreateMockS3ClientFactory ( ) ;
341+
342+ byte [ ] capturedBytes = null ;
343+ var mockTempFileFactory = new Mock < ITempFileFactory > ( ) ;
344+ var mockTempFile = new Mock < ITempFile > ( ) ;
345+ mockTempFile . Setup ( t => t . File ) . Returns ( new FileInfo ( "Files/minidump.dmp" ) ) ;
346+ mockTempFileFactory
347+ . Setup ( f => f . CreateFromBytes ( "feedback.json" , It . IsAny < byte [ ] > ( ) ) )
348+ . Callback < string , byte [ ] > ( ( name , bytes ) => capturedBytes = bytes )
349+ . Returns ( mockTempFile . Object ) ;
350+ mockTempFileFactory
351+ . Setup ( f => f . CreateTempZip ( It . IsAny < IEnumerable < FileInfo > > ( ) ) )
352+ . Returns ( mockTempFile . Object ) ;
353+ mockTempFile
354+ . Setup ( t => t . CreateFileStream ( It . IsAny < FileMode > ( ) , It . IsAny < FileAccess > ( ) ) )
355+ . Returns ( new MemoryStream ( new byte [ ] { 0 } ) ) ;
356+
357+ var sut = new CrashPostClient ( httpClientFactory , mockS3ClientFactory ) ;
358+ sut . TempFileFactory = mockTempFileFactory . Object ;
359+
360+ await sut . PostFeedback (
361+ database ,
362+ application ,
363+ version ,
364+ "Title with \" quotes\" " ,
365+ FeedbackPostOptions . Create ( bugsplat )
366+ ) ;
367+
368+ Assert . IsNotNull ( capturedBytes ) ;
369+ var feedbackJson = Encoding . UTF8 . GetString ( capturedBytes ) ;
370+ Assert . That ( feedbackJson , Does . Contain ( "Title with \\ \" quotes\\ \" " ) ) ;
371+ Assert . That ( feedbackJson , Does . Contain ( "Line1\\ nLine2\\ twith \\ \" quotes\\ \" " ) ) ;
372+ }
373+
374+ [ Test ]
375+ public async Task CrashPostClient_PostFeedback_ShouldUseOverrideDescription ( )
376+ {
377+ var bugsplat = new BugSplat ( database , application , version ) ;
378+ bugsplat . Description = "Default description" ;
379+ var getCrashUrl = "https://fake.url.com" ;
380+ var mockHttp = CreateMockHttpClientForExceptionPost ( getCrashUrl ) ;
381+ var httpClient = new HttpClient ( mockHttp . Object ) ;
382+ var httpClientFactory = new FakeHttpClientFactory ( httpClient ) ;
383+ var mockS3ClientFactory = FakeS3ClientFactory . CreateMockS3ClientFactory ( ) ;
384+
385+ byte [ ] capturedBytes = null ;
386+ var mockTempFileFactory = new Mock < ITempFileFactory > ( ) ;
387+ var mockTempFile = new Mock < ITempFile > ( ) ;
388+ mockTempFile . Setup ( t => t . File ) . Returns ( new FileInfo ( "Files/minidump.dmp" ) ) ;
389+ mockTempFileFactory
390+ . Setup ( f => f . CreateFromBytes ( "feedback.json" , It . IsAny < byte [ ] > ( ) ) )
391+ . Callback < string , byte [ ] > ( ( name , bytes ) => capturedBytes = bytes )
392+ . Returns ( mockTempFile . Object ) ;
393+ mockTempFileFactory
394+ . Setup ( f => f . CreateTempZip ( It . IsAny < IEnumerable < FileInfo > > ( ) ) )
395+ . Returns ( mockTempFile . Object ) ;
396+ mockTempFile
397+ . Setup ( t => t . CreateFileStream ( It . IsAny < FileMode > ( ) , It . IsAny < FileAccess > ( ) ) )
398+ . Returns ( new MemoryStream ( new byte [ ] { 0 } ) ) ;
399+
400+ var sut = new CrashPostClient ( httpClientFactory , mockS3ClientFactory ) ;
401+ sut . TempFileFactory = mockTempFileFactory . Object ;
402+
403+ var overrideOptions = new FeedbackPostOptions
404+ {
405+ Description = "Override description"
406+ } ;
407+
408+ await sut . PostFeedback (
409+ database ,
410+ application ,
411+ version ,
412+ "Test title" ,
413+ FeedbackPostOptions . Create ( bugsplat ) ,
414+ overrideOptions
415+ ) ;
416+
417+ Assert . IsNotNull ( capturedBytes ) ;
418+ var feedbackJson = Encoding . UTF8 . GetString ( capturedBytes ) ;
419+ Assert . That ( feedbackJson , Does . Contain ( "Override description" ) ) ;
420+ Assert . That ( feedbackJson , Does . Not . Contain ( "Default description" ) ) ;
421+ }
422+
423+ private Mock < HttpMessageHandler > CreateMockHttpClientWithCapture ( string crashUploadUrl , Action < HttpRequestMessage > captureCallback )
424+ {
425+ var getCrashUploadUrlResponse = new HttpResponseMessage ( ) ;
426+ getCrashUploadUrlResponse . StatusCode = HttpStatusCode . OK ;
427+ getCrashUploadUrlResponse . Content = new StringContent ( $ "{{ \" url\" : \" { crashUploadUrl } \" }}") ;
428+
429+ var commitCrashUploadUrlReponse = new HttpResponseMessage ( ) ;
430+ commitCrashUploadUrlReponse . StatusCode = HttpStatusCode . OK ;
431+
432+ var callCount = 0 ;
433+ var handlerMock = new Mock < HttpMessageHandler > ( MockBehavior . Strict ) ;
434+ handlerMock
435+ . Protected ( )
436+ . Setup < Task < HttpResponseMessage > > (
437+ "SendAsync" ,
438+ ItExpr . IsAny < HttpRequestMessage > ( ) ,
439+ ItExpr . IsAny < CancellationToken > ( )
440+ )
441+ . ReturnsAsync ( ( HttpRequestMessage request , CancellationToken token ) =>
442+ {
443+ callCount ++ ;
444+ if ( callCount == 1 )
445+ return getCrashUploadUrlResponse ;
446+ captureCallback ( request ) ;
447+ return commitCrashUploadUrlReponse ;
448+ } ) ;
449+
450+ return handlerMock ;
451+ }
452+
280453 private Mock < HttpMessageHandler > CreateMockHttpClientForExceptionPost ( string crashUploadUrl )
281454 {
282455 var getCrashUploadUrlResponse = new HttpResponseMessage ( ) ;
0 commit comments