diff --git a/BugSplatDotNetStandard.Test/Api/CrashPostClient.cs b/BugSplatDotNetStandard.Test/Api/CrashPostClient.cs index e67fc47..2f6f982 100644 --- a/BugSplatDotNetStandard.Test/Api/CrashPostClient.cs +++ b/BugSplatDotNetStandard.Test/Api/CrashPostClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Net; using System.Net.Http; using System.Threading; @@ -35,34 +36,20 @@ public void CrashPostClient_Constructor_ShouldThrowIfS3ClientFactoryIsNullOrEmpt } [Test] - public void CrashPostClient_PostException_ShouldCallPostAsyncWithUriAndMultipartFormDataContent() + public async Task CrashPostClient_PostException_ShouldReturn200() { - var expectedUri = $"https://{database}.bugsplat.com/post/dotnetstandard/"; + var expectedUri = $"https://{database}.bugsplat.com/api/getCrashUploadUrl?database={database}&appName={application}&appVersion={version}"; var stackTrace = CreateStackTrace(); var bugsplat = new BugSplat(database, application, version); - bugsplat.Description = "dangit bobby"; - bugsplat.Email = "bobby@bugsplat.com"; - bugsplat.IpAddress = "192.168.0.1"; - bugsplat.Key = "en-US"; - bugsplat.User = "@bobbyg603"; - var expectedFormDataParams = new List() { - "name=database", database, - "name=appName", application, - "name=appVersion", version, - "name=description", bugsplat.Description, - "name=email", bugsplat.Email, - "name=appKey", bugsplat.Key, - "name=user", bugsplat.User, - "name=callstack", stackTrace, - "name=crashTypeId", $"{(int)bugsplat.ExceptionType}" - }; - var mockHttp = CreateMockHttpClientForAuthenticateSuccess(); + var getCrashUrl = "https://fake.url.com"; + var mockHttp = CreateMockHttpClientForExceptionPost(getCrashUrl); var httpClient = new HttpClient(mockHttp.Object); var httpClientFactory = new FakeHttpClientFactory(httpClient); + var mockS3ClientFactory = FakeS3ClientFactory.CreateMockS3ClientFactory(); - var sut = new CrashPostClient(httpClientFactory, S3ClientFactory.Default); + var sut = new CrashPostClient(httpClientFactory, mockS3ClientFactory); - var postResult = sut.PostException( + var postResult = await sut.PostException( database, application, version, @@ -70,90 +57,54 @@ public void CrashPostClient_PostException_ShouldCallPostAsyncWithUriAndMultipart bugsplat ); - mockHttp.Protected().Verify( - "SendAsync", - Times.Exactly(1), - ItExpr.Is(req => - req.Method == HttpMethod.Post - && req.RequestUri.ToString().Equals(expectedUri) - && ContainsValues( - req.Content.ReadAsStringAsync().Result, - expectedFormDataParams - ) - ), - ItExpr.IsAny() - ); + Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); } [Test] - public void CrashPostClient_PostException_ShouldCallPostAsyncWithUriAndMultipartFormDataContentOverrides() + public async Task CrashPostClient_PostMinidump_ShouldReturn200() { - var stackTrace = CreateStackTrace(); + var expectedUri = $"https://{database}.bugsplat.com/api/getCrashUploadUrl?database={database}&appName={application}&appVersion={version}"; var bugsplat = new BugSplat(database, application, version); - var overrideOptions = new ExceptionPostOptions(); - overrideOptions.Description = "dangit bobby"; - overrideOptions.Email = "bobby@bugsplat.com"; - overrideOptions.IpAddress = "192.168.0.1"; - overrideOptions.Key = "en-US"; - overrideOptions.User = "@bobbyg603"; - overrideOptions.ExceptionType = BugSplat.ExceptionTypeId.Unity; - var expectedFormDataParams = new List() { - "name=database", database, - "name=appName", application, - "name=appVersion", version, - "name=description", overrideOptions.Description, - "name=email", overrideOptions.Email, - "name=appKey", overrideOptions.Key, - "name=user", overrideOptions.User, - "name=callstack", stackTrace, - "name=crashTypeId", $"{(int)overrideOptions.ExceptionType}" - }; - var mockHttp = CreateMockHttpClientForAuthenticateSuccess(); + var getCrashUrl = "https://fake.url.com"; + var mockHttp = CreateMockHttpClientForExceptionPost(getCrashUrl); var httpClient = new HttpClient(mockHttp.Object); var httpClientFactory = new FakeHttpClientFactory(httpClient); + var mockS3ClientFactory = FakeS3ClientFactory.CreateMockS3ClientFactory(); - var sut = new CrashPostClient(httpClientFactory, S3ClientFactory.Default); + var sut = new CrashPostClient(httpClientFactory, mockS3ClientFactory); - var postResult = sut.PostException( + var postResult = await sut.PostMinidump( database, application, version, - stackTrace, - bugsplat, - overrideOptions + new FileInfo("Files/minidump.dmp"), + bugsplat ); - mockHttp.Protected().Verify( - "SendAsync", - Times.Exactly(1), - ItExpr.Is(req => - req.Method == HttpMethod.Post - && ContainsValues( - req.Content.ReadAsStringAsync().Result, - expectedFormDataParams - ) - ), - ItExpr.IsAny() - ); + Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); } - private Mock CreateMockHttpClientForAuthenticateSuccess() + private Mock CreateMockHttpClientForExceptionPost(string crashUploadUrl) { - var response = new HttpResponseMessage(); - response.Content = new StringContent(""); - response.StatusCode = HttpStatusCode.OK; + var getCrashUploadUrlResponse = new HttpResponseMessage(); + getCrashUploadUrlResponse.StatusCode = HttpStatusCode.OK; + getCrashUploadUrlResponse.Content = new StringContent($"{{ \"url\": \"{crashUploadUrl}\" }}"); + + var commitCrashUploadUrlReponse = new HttpResponseMessage(); + commitCrashUploadUrlReponse.StatusCode = HttpStatusCode.OK; + var handlerMock = new Mock(MockBehavior.Strict); handlerMock .Protected() - .Setup>( + .SetupSequence>( "SendAsync", ItExpr.IsAny(), ItExpr.IsAny() ) - .ReturnsAsync(response) - .Verifiable(); + .ReturnsAsync(getCrashUploadUrlResponse) + .ReturnsAsync(commitCrashUploadUrlReponse); return handlerMock; } } -} \ No newline at end of file +} diff --git a/BugSplatDotNetStandard.Test/Api/VersionsClient.cs b/BugSplatDotNetStandard.Test/Api/VersionsClient.cs index 5169c48..f7da29a 100644 --- a/BugSplatDotNetStandard.Test/Api/VersionsClient.cs +++ b/BugSplatDotNetStandard.Test/Api/VersionsClient.cs @@ -91,7 +91,7 @@ public void VersionsClient_UploadSymbolsFile_ShouldDeleteZipFileAfterUpload() var zipFileFullName = "test.zip"; var symbolFileInfo = new FileInfo("Files/myConsoleCrasher.pdb"); var mockApiClient = CreateMockBugSplatApiClient(); - var mockS3ClientFactory = CreateMockS3ClientFactory(); + var mockS3ClientFactory = FakeS3ClientFactory.CreateMockS3ClientFactory(); var mockZipUtils = CreateMockZipUtils(zipFileFullName); var sut = new VersionsClient(mockApiClient, mockS3ClientFactory); sut.ZipUtils = mockZipUtils; @@ -106,19 +106,6 @@ public void VersionsClient_UploadSymbolsFile_ShouldDeleteZipFileAfterUpload() Assert.False(File.Exists(zipFileFullName)); } - private IS3ClientFactory CreateMockS3ClientFactory() - { - var s3Response = new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK - }; - var mockS3Client = new Mock(); - mockS3Client - .Setup(s => s.UploadFileStreamToPresignedURL(It.IsAny(), It.IsAny())) - .Returns(() => Task.Factory.StartNew(() => s3Response)); - return new FakeS3ClientFactory(mockS3Client.Object); - } - private IBugSplatApiClient CreateMockBugSplatApiClient() { var presignedUrlResponse = new HttpResponseMessage() @@ -131,7 +118,7 @@ private IBugSplatApiClient CreateMockBugSplatApiClient() .Returns(true); mockApiClient .Setup(c => c.PostAsync(It.IsAny(), It.IsAny())) - .Returns(() => Task.Factory.StartNew(() => presignedUrlResponse)); + .ReturnsAsync(presignedUrlResponse); return mockApiClient.Object; } @@ -146,7 +133,7 @@ private IZipUtils CreateMockZipUtils(string zipFileFullName) var mockZipUtils = new Mock(); mockZipUtils .Setup(z => z.CreateZipFileFullName(It.IsAny())) - .Returns(() => zipFileFullName); + .Returns(zipFileFullName); mockZipUtils .Setup(z => z.CreateZipFile(It.IsAny(), It.IsAny>())) .Returns(zipFileInfo); diff --git a/BugSplatDotNetStandard.Test/BugSplat.cs b/BugSplatDotNetStandard.Test/BugSplat.cs index 26e64a5..40876e5 100644 --- a/BugSplatDotNetStandard.Test/BugSplat.cs +++ b/BugSplatDotNetStandard.Test/BugSplat.cs @@ -89,13 +89,15 @@ public void BugSplat_Post_ShouldPostExceptionToBugSplat() sut.Email = "default@bugsplat.com - overridden"; sut.User = "Default - overridden"; sut.Key = "Default - overridden"; + sut.Notes = "Default - overridden"; var options = new ExceptionPostOptions() { ExceptionType = BugSplat.ExceptionTypeId.DotNetStandard, Description = "BugSplat rocks!", Email = "fred@bugsplat.com", User = "Fred", - Key = "the key!" + Key = "the key!", + Notes = "the notes!" }; options.Attachments.Add(new FileInfo("Files/attachment.txt")); var response = sut.Post(ex, options).Result; @@ -115,13 +117,15 @@ public void BugSplat_Post_ShouldPostMinidumpToBugSplat() sut.Email = "default@bugsplat.com - overridden"; sut.User = "Default - overridden"; sut.Key = "Default - overridden"; + sut.Notes = "Default - overridden"; var options = new MinidumpPostOptions() { MinidumpType = BugSplat.MinidumpTypeId.UnityNativeWindows, Description = "BugSplat rocks!", Email = "fred@bugsplat.com", User = "Fred", - Key = "the key!" + Key = "the key!", + Notes = "the notes!" }; options.Attachments.Add(new FileInfo("Files/attachment.txt")); @@ -151,6 +155,7 @@ public void BugSplat_Post_ShouldPostStackTraceToBugSplat() sut.Email = "default@bugsplat.com - overridden"; sut.User = "Default - overridden"; sut.Key = "Default - overridden"; + sut.Notes = "Default - overridden"; var stackTrace = CreateStackTrace(); var options = new ExceptionPostOptions() { @@ -158,7 +163,8 @@ public void BugSplat_Post_ShouldPostStackTraceToBugSplat() Description = "BugSplat rocks!", Email = "fred@bugsplat.com", User = "Fred", - Key = "the key!" + Key = "the key!", + Notes = "the notes!" }; options.Attachments.Add(new FileInfo("Files/attachment.txt")); var response = sut.Post(stackTrace, options).Result; diff --git a/BugSplatDotNetStandard.Test/Helpers/FakeS3ClientFactory.cs b/BugSplatDotNetStandard.Test/Helpers/FakeS3ClientFactory.cs index 06c4642..f0b59c1 100644 --- a/BugSplatDotNetStandard.Test/Helpers/FakeS3ClientFactory.cs +++ b/BugSplatDotNetStandard.Test/Helpers/FakeS3ClientFactory.cs @@ -1,4 +1,9 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Http; using BugSplatDotNetStandard.Http; +using Moq; namespace Tests { @@ -14,5 +19,28 @@ public IS3Client CreateClient() { return this.client; } + + public static IS3ClientFactory CreateMockS3ClientFactory() + { + var s3UploadFileStreamResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK + }; + var s3UploadFileBytesResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + }; + s3UploadFileBytesResponse.Headers.Add("ETag", "\"test\""); + + var mockS3Client = new Mock(); + mockS3Client + .Setup(s => s.UploadFileStreamToPresignedURL(It.IsAny(), It.IsAny())) + .ReturnsAsync(s3UploadFileStreamResponse); + mockS3Client + .Setup(s => s.UploadFileBytesToPresignedURL(It.IsAny(), It.IsAny())) + .ReturnsAsync(s3UploadFileBytesResponse); + + return new FakeS3ClientFactory(mockS3Client.Object); + } } } \ No newline at end of file diff --git a/BugSplatDotNetStandard/Api/CrashPostClient.cs b/BugSplatDotNetStandard/Api/CrashPostClient.cs index 8d5e5ed..51305f4 100644 --- a/BugSplatDotNetStandard/Api/CrashPostClient.cs +++ b/BugSplatDotNetStandard/Api/CrashPostClient.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net.Http; using System.Net.Http.Headers; +using System.Text; using System.Threading.Tasks; using BugSplatDotNetStandard.Http; using BugSplatDotNetStandard.Utils; @@ -42,14 +43,49 @@ public async Task PostException( { overridePostOptions = overridePostOptions ?? new ExceptionPostOptions(); - var baseUrl = this.CreateBaseUrlFromDatabase(database); - var uri = new Uri($"{baseUrl}/post/dotnetstandard/"); - var body = CreateMultiPartFormDataContent(database, application, version, defaultPostOptions, overridePostOptions); - var crashTypeId = overridePostOptions?.ExceptionType != ExceptionTypeId.Unknown ? overridePostOptions.ExceptionType : defaultPostOptions.ExceptionType; - body.Add(new StringContent(stackTrace), "callstack"); - body.Add(new StringContent($"{(int)crashTypeId}"), "crashTypeId"); + var files = overridePostOptions.Attachments + .Select(attachment => InMemoryFile.FromFileInfo(attachment)) + .ToList(); + + files.Add(new InMemoryFile() { FileName = "Callstack.txt", Content = Encoding.UTF8.GetBytes(stackTrace) }); + + var zipBytes = ZipUtils.CreateInMemoryZipFile(files); + using ( + var crashUploadResponse = await GetCrashUploadUrl( + database, + application, + version, + zipBytes.Length + ) + ) + { + ThrowIfHttpRequestFailed(crashUploadResponse); + + var presignedUrl = await GetPresignedUrlFromResponse(crashUploadResponse); + + using (var uploadFileResponse = await this.s3Client.UploadFileBytesToPresignedURL(presignedUrl, zipBytes)) + { + ThrowIfHttpRequestFailed(uploadFileResponse); - return await httpClient.PostAsync(uri, body); + var s3Key = presignedUrl.ToString(); + var md5 = GetETagFromResponseHeaders(uploadFileResponse.Headers); + var crashTypeId = (int)(overridePostOptions?.ExceptionType != ExceptionTypeId.Unknown ? overridePostOptions.ExceptionType : defaultPostOptions.ExceptionType); + var commitS3CrashResponse = await CommitS3CrashUpload( + database, + application, + version, + md5, + s3Key, + crashTypeId, + defaultPostOptions, + overridePostOptions + ); + + ThrowIfHttpRequestFailed(commitS3CrashResponse); + + return commitS3CrashResponse; + } + } } public async Task PostMinidump( @@ -63,9 +99,11 @@ public async Task PostMinidump( { overridePostOptions = overridePostOptions ?? new MinidumpPostOptions(); - var files = new List(); - files.Add(minidumpFileInfo); - files.AddRange(overridePostOptions.Attachments); + var files = overridePostOptions.Attachments + .Select(attachment => InMemoryFile.FromFileInfo(attachment)) + .ToList(); + + files.Add(new InMemoryFile() { FileName = minidumpFileInfo.Name, Content = File.ReadAllBytes(minidumpFileInfo.FullName) }); var zipBytes = ZipUtils.CreateInMemoryZipFile(files); using ( @@ -87,12 +125,14 @@ public async Task PostMinidump( var s3Key = presignedUrl.ToString(); var md5 = GetETagFromResponseHeaders(uploadFileResponse.Headers); + var crashTypeId = (int)(overridePostOptions?.MinidumpType != MinidumpTypeId.Unknown ? overridePostOptions.MinidumpType : defaultPostOptions.MinidumpType); var commitS3CrashResponse = await CommitS3CrashUpload( database, application, version, md5, s3Key, + crashTypeId, defaultPostOptions, overridePostOptions ); @@ -116,13 +156,13 @@ private async Task CommitS3CrashUpload( string version, string md5, string s3Key, - IMinidumpPostOptions defaultPostOptions, - IMinidumpPostOptions overridePostOptions + int crashTypeId, + IBugSplatPostOptions defaultPostOptions, + IBugSplatPostOptions overridePostOptions ) { var baseUrl = this.CreateBaseUrlFromDatabase(database); var route = $"{baseUrl}/api/commitS3CrashUpload"; - var crashTypeId = overridePostOptions?.MinidumpType != MinidumpTypeId.Unknown ? overridePostOptions.MinidumpType : defaultPostOptions.MinidumpType; var body = CreateMultiPartFormDataContent( database, application, @@ -130,7 +170,7 @@ IMinidumpPostOptions overridePostOptions defaultPostOptions, overridePostOptions ); - body.Add(new StringContent($"{(int)crashTypeId}"), "crashTypeId"); + body.Add(new StringContent($"{crashTypeId}"), "crashTypeId"); body.Add(new StringContent(s3Key), "s3Key"); body.Add(new StringContent(md5), "md5"); @@ -153,6 +193,7 @@ private MultipartFormDataContent CreateMultiPartFormDataContent( var description = GetStringValueOrDefault(overrideOptions?.Description, defaultOptions.Description); var email = GetStringValueOrDefault(overrideOptions?.Email, defaultOptions.Email); var key = GetStringValueOrDefault(overrideOptions?.Key, defaultOptions.Key); + var notes = GetStringValueOrDefault(overrideOptions?.Notes, defaultOptions.Notes); var user = GetStringValueOrDefault(overrideOptions?.User, defaultOptions.User); var body = new MultipartFormDataContent @@ -163,7 +204,8 @@ private MultipartFormDataContent CreateMultiPartFormDataContent( { new StringContent(description), "description" }, { new StringContent(email), "email" }, { new StringContent(key), "appKey" }, - { new StringContent(user), "user" } + { new StringContent(notes), "notes" }, + { new StringContent(user), "user" }, }; var formDataParams = overrideOptions?.FormDataParams ?? new List(); diff --git a/BugSplatDotNetStandard/BugSplat.cs b/BugSplatDotNetStandard/BugSplat.cs index b3e22a3..b9c194a 100644 --- a/BugSplatDotNetStandard/BugSplat.cs +++ b/BugSplatDotNetStandard/BugSplat.cs @@ -25,12 +25,12 @@ public class BugSplat: IExceptionPostOptions, IMinidumpPostOptions public ExceptionTypeId ExceptionType { get; set; } = ExceptionTypeId.DotNetStandard; /// - /// A default description added to the upload that can be overriden at post time + /// A default description added to the upload that can be overridden at post time /// public string Description { get; set; } = string.Empty; /// - /// A default email added to the upload that can be overriden at post time + /// A default email added to the upload that can be overridden at post time /// public string Email { get; set; } = string.Empty; @@ -40,12 +40,12 @@ public class BugSplat: IExceptionPostOptions, IMinidumpPostOptions public List FormDataParams { get; } = new List(); /// - /// A default key added to the upload that can be overriden at post time + /// A default key added to the upload that can be overridden at post time /// public string Key { get; set; } = string.Empty; /// - /// A default IP Address value added to the upload that can be overriden at post time + /// A default IP Address value added to the upload that can be overridden at post time /// public string IpAddress { get; set; } = string.Empty; @@ -55,7 +55,12 @@ public class BugSplat: IExceptionPostOptions, IMinidumpPostOptions public MinidumpTypeId MinidumpType { get; set; } = MinidumpTypeId.WindowsNative; /// - /// A default user added to the upload that can be overriden at post time + /// An general purpose column for extra crash metadata + /// + public string Notes { get; set; } = string.Empty; + + /// + /// A default user added to the upload that can be overridden at post time /// public string User { get; set; } = string.Empty; diff --git a/BugSplatDotNetStandard/BugSplatPostOptions.cs b/BugSplatDotNetStandard/BugSplatPostOptions.cs index 64b2ccb..ea356aa 100644 --- a/BugSplatDotNetStandard/BugSplatPostOptions.cs +++ b/BugSplatDotNetStandard/BugSplatPostOptions.cs @@ -25,6 +25,7 @@ public abstract class BugSplatPostOptions : IBugSplatPostOptions public string Email { get; set; } = string.Empty; public string Key { get; set; } = string.Empty; public string IpAddress { get; set; } = string.Empty; + public string Notes { get; set; } = string.Empty; public string User { get; set; } = string.Empty; } @@ -76,6 +77,11 @@ public interface IBugSplatPostOptions /// string IpAddress { get; set; } + /// + /// An general purpose column for extra crash metadata + /// + string Notes { get; set; } + /// /// A user to be added to the post that overrides the corresponding default value /// diff --git a/BugSplatDotNetStandard/Utils/ZipUtils.cs b/BugSplatDotNetStandard/Utils/ZipUtils.cs index b011e64..4331b3e 100644 --- a/BugSplatDotNetStandard/Utils/ZipUtils.cs +++ b/BugSplatDotNetStandard/Utils/ZipUtils.cs @@ -8,11 +8,20 @@ internal class InMemoryFile { public string FileName { get; set; } public byte[] Content { get; set; } + + public static InMemoryFile FromFileInfo(FileInfo fileInfo) + { + return new InMemoryFile() + { + FileName = fileInfo.Name, + Content = File.ReadAllBytes(fileInfo.FullName) + }; + } } internal interface IZipUtils { - byte[] CreateInMemoryZipFile(IEnumerable files); + byte[] CreateInMemoryZipFile(IEnumerable files); FileInfo CreateZipFile(string zipFileFullName, IEnumerable files); string CreateZipFileFullName(string inputFileName); Stream CreateZipFileStream(string zipFileFullName); @@ -21,20 +30,14 @@ internal interface IZipUtils internal class ZipUtils: IZipUtils { - public byte[] CreateInMemoryZipFile(IEnumerable files) + public byte[] CreateInMemoryZipFile(IEnumerable files) { - var inMemoryFiles = new List(); - foreach (var attachment in files) - { - inMemoryFiles.Add(new InMemoryFile() { FileName = attachment.Name, Content = File.ReadAllBytes(attachment.FullName) }); - } - byte[] zipBytes; using (var archiveStream = new MemoryStream()) { using (var archive = new ZipArchive(archiveStream, ZipArchiveMode.Create, true)) { - foreach (var file in inMemoryFiles) + foreach (var file in files) { var zipArchiveEntry = archive.CreateEntry(file.FileName, CompressionLevel.Fastest); using (var zipStream = zipArchiveEntry.Open())