diff --git a/Source/CakeMail.RestClient.IntegrationTests/CakeMail.RestClient.IntegrationTests.csproj b/Source/CakeMail.RestClient.IntegrationTests/CakeMail.RestClient.IntegrationTests.csproj index 009c51e..66703d2 100644 --- a/Source/CakeMail.RestClient.IntegrationTests/CakeMail.RestClient.IntegrationTests.csproj +++ b/Source/CakeMail.RestClient.IntegrationTests/CakeMail.RestClient.IntegrationTests.csproj @@ -11,6 +11,10 @@ latest + + + + diff --git a/Source/CakeMail.RestClient.IntegrationTests/ConsoleUtils.cs b/Source/CakeMail.RestClient.IntegrationTests/ConsoleUtils.cs new file mode 100644 index 0000000..966c9e0 --- /dev/null +++ b/Source/CakeMail.RestClient.IntegrationTests/ConsoleUtils.cs @@ -0,0 +1,32 @@ +using System; + +namespace CakeMail.RestClient.IntegrationTests +{ + public static class ConsoleUtils + { + public static void CenterConsole() + { + var hWin = NativeMethods.GetConsoleWindow(); + if (hWin == IntPtr.Zero) return; + + var monitor = NativeMethods.MonitorFromWindow(hWin, NativeMethods.MONITOR_DEFAULT_TO_NEAREST); + if (monitor == IntPtr.Zero) return; + + var monitorInfo = new NativeMethods.NativeMonitorInfo(); + NativeMethods.GetMonitorInfo(monitor, monitorInfo); + + NativeMethods.GetWindowRect(hWin, out NativeMethods.NativeRectangle consoleInfo); + + var monitorWidth = monitorInfo.Monitor.Right - monitorInfo.Monitor.Left; + var monitorHeight = monitorInfo.Monitor.Bottom - monitorInfo.Monitor.Top; + + var consoleWidth = consoleInfo.Right - consoleInfo.Left; + var consoleHeight = consoleInfo.Bottom - consoleInfo.Top; + + var left = monitorInfo.Monitor.Left + ((monitorWidth - consoleWidth) / 2); + var top = monitorInfo.Monitor.Top + ((monitorHeight - consoleHeight) / 2); + + NativeMethods.MoveWindow(hWin, left, top, consoleWidth, consoleHeight, false); + } + } +} diff --git a/Source/CakeMail.RestClient.IntegrationTests/IIntegrationTest.cs b/Source/CakeMail.RestClient.IntegrationTests/IIntegrationTest.cs new file mode 100644 index 0000000..c797cd9 --- /dev/null +++ b/Source/CakeMail.RestClient.IntegrationTests/IIntegrationTest.cs @@ -0,0 +1,11 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace CakeMail.RestClient.IntegrationTests +{ + public interface IIntegrationTest + { + Task Execute(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken); + } +} diff --git a/Source/CakeMail.RestClient.IntegrationTests/NativeMethods.cs b/Source/CakeMail.RestClient.IntegrationTests/NativeMethods.cs new file mode 100644 index 0000000..1fe8718 --- /dev/null +++ b/Source/CakeMail.RestClient.IntegrationTests/NativeMethods.cs @@ -0,0 +1,52 @@ +using System; +using System.Runtime.InteropServices; + +namespace CakeMail.RestClient.IntegrationTests +{ + public static class NativeMethods + { + public const Int32 MONITOR_DEFAULT_TO_PRIMARY = 0x00000001; + public const Int32 MONITOR_DEFAULT_TO_NEAREST = 0x00000002; + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr GetConsoleWindow(); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool GetWindowRect(IntPtr hWnd, out NativeRectangle rc); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool MoveWindow(IntPtr hWnd, int x, int y, int w, int h, bool repaint); + + [DllImport("user32.dll")] + public static extern IntPtr MonitorFromWindow(IntPtr handle, Int32 flags); + + [DllImport("user32.dll")] + public static extern Boolean GetMonitorInfo(IntPtr hMonitor, NativeMonitorInfo lpmi); + + [Serializable, StructLayout(LayoutKind.Sequential)] + public struct NativeRectangle + { + public Int32 Left; + public Int32 Top; + public Int32 Right; + public Int32 Bottom; + + public NativeRectangle(Int32 left, Int32 top, Int32 right, Int32 bottom) + { + this.Left = left; + this.Top = top; + this.Right = right; + this.Bottom = bottom; + } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public sealed class NativeMonitorInfo + { + public Int32 Size = Marshal.SizeOf(typeof(NativeMonitorInfo)); + public NativeRectangle Monitor; + public NativeRectangle Work; + public Int32 Flags; + } + } +} diff --git a/Source/CakeMail.RestClient.IntegrationTests/Program.cs b/Source/CakeMail.RestClient.IntegrationTests/Program.cs index 5665c82..ff9306b 100644 --- a/Source/CakeMail.RestClient.IntegrationTests/Program.cs +++ b/Source/CakeMail.RestClient.IntegrationTests/Program.cs @@ -1,7 +1,12 @@ using CakeMail.RestClient.IntegrationTests.Tests; -using CakeMail.RestClient.Logging; +using CakeMail.RestClient.Utilities; +using Logzio.DotNet.NLog; +using NLog; +using NLog.Config; +using NLog.Targets; using System; using System.IO; +using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -10,30 +15,63 @@ namespace CakeMail.RestClient.IntegrationTests { public class Program { + private const int MAX_CAKEMAIL_API_CONCURRENCY = 5; + + private enum ResultCodes + { + Success = 0, + Exception = 1, + Cancelled = 1223 + } + static async Task Main() { // ----------------------------------------------------------------------------- - - // Do you want to proxy requests through Fiddler (useful for debugging)? + // Do you want to proxy requests through Fiddler? Can be useful for debugging. var useFiddler = false; - // As an alternative to Fiddler, you can display debug information about - // every HTTP request/response in the console. This is useful for debugging - // purposes but the amount of information can be overwhelming. - var debugHttpMessagesToConsole = false; + // Logging options. + var options = new CakeMailClientOptions() + { + LogLevelFailedCalls = CakeMail.RestClient.Logging.LogLevel.Error, + LogLevelSuccessfulCalls = CakeMail.RestClient.Logging.LogLevel.Debug + }; // ----------------------------------------------------------------------------- - var proxy = useFiddler ? new WebProxy("http://localhost:8888") : null; + // Configure logging + var nLogConfig = new LoggingConfiguration(); + + // Send logs to logz.io + var logzioToken = Environment.GetEnvironmentVariable("LOGZIO_TOKEN"); + if (!string.IsNullOrEmpty(logzioToken)) + { + var logzioTarget = new LogzioTarget { Token = logzioToken }; + logzioTarget.ContextProperties.Add(new TargetPropertyWithContext("source", "StrongGrid_integration_tests")); + logzioTarget.ContextProperties.Add(new TargetPropertyWithContext("StrongGrid-Version", CakeMailRestClient.Version)); + + nLogConfig.AddTarget("Logzio", logzioTarget); + nLogConfig.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, "Logzio", "*"); + } + + // Send logs to console + var consoleTarget = new ColoredConsoleTarget(); + nLogConfig.AddTarget("ColoredConsole", consoleTarget); + nLogConfig.AddRule(NLog.LogLevel.Warn, NLog.LogLevel.Fatal, "ColoredConsole", "*"); + + LogManager.Configuration = nLogConfig; + + // Configure CakeMail client var apiKey = Environment.GetEnvironmentVariable("CAKEMAIL_APIKEY"); var userName = Environment.GetEnvironmentVariable("CAKEMAIL_USERNAME"); var password = Environment.GetEnvironmentVariable("CAKEMAIL_PASSWORD"); var overrideClientId = Environment.GetEnvironmentVariable("CAKEMAIL_OVERRIDECLIENTID"); + var proxy = useFiddler ? new WebProxy("http://localhost:8888") : null; + var client = new CakeMailRestClient(apiKey, proxy); + var loginInfo = client.Users.LoginAsync(userName, password).Result; + var clientId = string.IsNullOrEmpty(overrideClientId) ? loginInfo.ClientId : long.Parse(overrideClientId); + var userKey = loginInfo.UserKey; - if (debugHttpMessagesToConsole) - { - LogProvider.SetCurrentLogProvider(new ColoredConsoleLogProvider()); - } - + // Configure Console var source = new CancellationTokenSource(); Console.CancelKeyPress += (s, e) => { @@ -41,55 +79,100 @@ static async Task Main() source.Cancel(); }; - try + // Ensure the Console is tall enough and centered on the screen + Console.WindowHeight = Math.Min(60, Console.LargestWindowHeight); + ConsoleUtils.CenterConsole(); + + // These are the integration tests that we will execute + var integrationTests = new Type[] { - var client = new CakeMailRestClient(apiKey, proxy); - var loginInfo = client.Users.LoginAsync(userName, password).Result; - var clientId = string.IsNullOrEmpty(overrideClientId) ? loginInfo.ClientId : long.Parse(overrideClientId); - var userKey = loginInfo.UserKey; + typeof(CampaignsTests), + typeof(ClientsTests), + typeof(CountriesTests), + typeof(ListsTests), + typeof(MailingsTests), + typeof(PermissionsTests), + typeof(RelaysTests), + typeof(SuppressionListsTests), + typeof(TemplatesTests), + typeof(TimezonesTests), + typeof(TriggersTests), + typeof(UsersTests), + }; - var tasks = new Task[] + // Execute the async tests in parallel (with max degree of parallelism) + var results = await integrationTests.ForEachAsync( + async integrationTestType => { - ExecuteAsync(client, userKey, clientId, source, TimezonesTests.ExecuteAllMethods), - ExecuteAsync(client, userKey, clientId, source, CountriesTests.ExecuteAllMethods), - ExecuteAsync(client, userKey, clientId, source, ClientsTests.ExecuteAllMethods), - ExecuteAsync(client, userKey, clientId, source, UsersTests.ExecuteAllMethods), - ExecuteAsync(client, userKey, clientId, source, PermissionsTests.ExecuteAllMethods), - ExecuteAsync(client, userKey, clientId, source, CampaignsTests.ExecuteAllMethods), - ExecuteAsync(client, userKey, clientId, source, ListsTests.ExecuteAllMethods), - ExecuteAsync(client, userKey, clientId, source, TemplatesTests.ExecuteAllMethods), - ExecuteAsync(client, userKey, clientId, source, SuppressionListsTests.ExecuteAllMethods), - ExecuteAsync(client, userKey, clientId, source, RelaysTests.ExecuteAllMethods), - ExecuteAsync(client, userKey, clientId, source, TriggersTests.ExecuteAllMethods), - ExecuteAsync(client, userKey, clientId, source, MailingsTests.ExecuteAllMethods) - }; - await Task.WhenAll(tasks).ConfigureAwait(false); - return await Task.FromResult(0); // Success. - } - catch (OperationCanceledException) + var log = new StringWriter(); + + try + { + var integrationTest = (IIntegrationTest)Activator.CreateInstance(integrationTestType); + await integrationTest.Execute(client, userKey, clientId, log, source.Token).ConfigureAwait(false); + return (TestName: integrationTestType.Name, ResultCode: ResultCodes.Success, Message: string.Empty); + } + catch (OperationCanceledException) + { + await log.WriteLineAsync($"-----> TASK CANCELLED").ConfigureAwait(false); + return (TestName: integrationTestType.Name, ResultCode: ResultCodes.Cancelled, Message: "Task cancelled"); + } + catch (Exception e) + { + var exceptionMessage = e.GetBaseException().Message; + await log.WriteLineAsync($"-----> AN EXCEPTION OCCURRED: {exceptionMessage}").ConfigureAwait(false); + return (TestName: integrationTestType.Name, ResultCode: ResultCodes.Exception, Message: exceptionMessage); + } + finally + { + await Console.Out.WriteLineAsync(log.ToString()).ConfigureAwait(false); + } + }, MAX_CAKEMAIL_API_CONCURRENCY) + .ConfigureAwait(false); + + // Display summary + var summary = new StringWriter(); + await summary.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); + await summary.WriteLineAsync("******************** SUMMARY *********************").ConfigureAwait(false); + await summary.WriteLineAsync("**************************************************").ConfigureAwait(false); + + var resultsWithMessage = results + .Where(r => !string.IsNullOrEmpty(r.Message)) + .ToArray(); + + if (resultsWithMessage.Any()) { - return 1223; // Cancelled. + foreach (var (TestName, ResultCode, Message) in resultsWithMessage) + { + const int TEST_NAME_MAX_LENGTH = 25; + var name = TestName.Length <= TEST_NAME_MAX_LENGTH ? TestName : TestName.Substring(0, TEST_NAME_MAX_LENGTH - 3) + "..."; + await summary.WriteLineAsync($"{name.PadRight(TEST_NAME_MAX_LENGTH, ' ')} : {Message}").ConfigureAwait(false); + } } - catch (Exception e) + else { - source.Cancel(); - var log = new StringWriter(); - await log.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); - await log.WriteLineAsync("**************************************************").ConfigureAwait(false); - await log.WriteLineAsync($"AN EXCEPTION OCCURED: {e.GetBaseException().Message}").ConfigureAwait(false); - await log.WriteLineAsync("**************************************************").ConfigureAwait(false); - await log.WriteLineAsync("**************************************************").ConfigureAwait(false); - await Console.Out.WriteLineAsync(log.ToString()).ConfigureAwait(false); - return 1; // Exception + await summary.WriteLineAsync("All tests completed succesfully").ConfigureAwait(false); } - finally + + await summary.WriteLineAsync("**************************************************").ConfigureAwait(false); + await Console.Out.WriteLineAsync(summary.ToString()).ConfigureAwait(false); + + // Prompt user to press a key in order to allow reading the log in the console + var promptLog = new StringWriter(); + await promptLog.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); + await promptLog.WriteLineAsync("Press any key to exit").ConfigureAwait(false); + Prompt(promptLog.ToString()); + + // Return code indicating success/failure + var resultCode = (int)ResultCodes.Success; + if (results.Any(result => result.ResultCode != ResultCodes.Success)) { - var log = new StringWriter(); - await log.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); - await log.WriteLineAsync("All tests completed").ConfigureAwait(false); - await log.WriteLineAsync("Press any key to exit").ConfigureAwait(false); - Prompt(log.ToString()); + if (results.Any(result => result.ResultCode == ResultCodes.Exception)) resultCode = (int)ResultCodes.Exception; + else if (results.Any(result => result.ResultCode == ResultCodes.Cancelled)) resultCode = (int)ResultCodes.Cancelled; + else resultCode = (int)results.First(result => result.ResultCode != ResultCodes.Success).ResultCode; } + + return await Task.FromResult(resultCode); } private static char Prompt(string prompt) @@ -102,31 +185,5 @@ private static char Prompt(string prompt) var result = Console.ReadKey(); return result.KeyChar; } - - private static async Task ExecuteAsync(ICakeMailRestClient client, string userKey, long clientId, CancellationTokenSource cts, Func asyncTask) - { - var log = new StringWriter(); - - try - { - await asyncTask(client, userKey, clientId, log, cts.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - await log.WriteLineAsync($"-----> TASK CANCELLED").ConfigureAwait(false); - return 1223; // Cancelled. - } - catch (Exception e) - { - await log.WriteLineAsync($"-----> AN EXCEPTION OCCURED: {e.GetBaseException().Message}").ConfigureAwait(false); - throw; - } - finally - { - await Console.Out.WriteLineAsync(log.ToString()).ConfigureAwait(false); - } - - return 0; // Success - } } } diff --git a/Source/CakeMail.RestClient.IntegrationTests/Tests/CampaignsTests.cs b/Source/CakeMail.RestClient.IntegrationTests/Tests/CampaignsTests.cs index 69371a0..8879e0f 100644 --- a/Source/CakeMail.RestClient.IntegrationTests/Tests/CampaignsTests.cs +++ b/Source/CakeMail.RestClient.IntegrationTests/Tests/CampaignsTests.cs @@ -6,9 +6,9 @@ namespace CakeMail.RestClient.IntegrationTests.Tests { - public static class CampaignsTests + public class CampaignsTests : IIntegrationTest { - public static async Task ExecuteAllMethods(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) + public async Task Execute(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) { await log.WriteLineAsync("\n***** CAMPAIGNS *****").ConfigureAwait(false); diff --git a/Source/CakeMail.RestClient.IntegrationTests/Tests/ClientsTests.cs b/Source/CakeMail.RestClient.IntegrationTests/Tests/ClientsTests.cs index af21a20..846cfd9 100644 --- a/Source/CakeMail.RestClient.IntegrationTests/Tests/ClientsTests.cs +++ b/Source/CakeMail.RestClient.IntegrationTests/Tests/ClientsTests.cs @@ -7,11 +7,11 @@ namespace CakeMail.RestClient.IntegrationTests.Tests { - public static class ClientsTests + public class ClientsTests : IIntegrationTest { private const int UTC_TIMEZONE_ID = 542; - public static async Task ExecuteAllMethods(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) + public async Task Execute(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) { await log.WriteLineAsync("\n***** CLIENT *****").ConfigureAwait(false); diff --git a/Source/CakeMail.RestClient.IntegrationTests/Tests/CountriesTests.cs b/Source/CakeMail.RestClient.IntegrationTests/Tests/CountriesTests.cs index 21679fb..c742250 100644 --- a/Source/CakeMail.RestClient.IntegrationTests/Tests/CountriesTests.cs +++ b/Source/CakeMail.RestClient.IntegrationTests/Tests/CountriesTests.cs @@ -5,9 +5,9 @@ namespace CakeMail.RestClient.IntegrationTests.Tests { - public static class CountriesTests + public class CountriesTests : IIntegrationTest { - public static async Task ExecuteAllMethods(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) + public async Task Execute(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) { await log.WriteLineAsync("\n***** COUNTRIES *****").ConfigureAwait(false); diff --git a/Source/CakeMail.RestClient.IntegrationTests/Tests/ListsTests.cs b/Source/CakeMail.RestClient.IntegrationTests/Tests/ListsTests.cs index 9cd7bb8..f9938cf 100644 --- a/Source/CakeMail.RestClient.IntegrationTests/Tests/ListsTests.cs +++ b/Source/CakeMail.RestClient.IntegrationTests/Tests/ListsTests.cs @@ -8,9 +8,9 @@ namespace CakeMail.RestClient.IntegrationTests.Tests { - public static class ListsTests + public class ListsTests : IIntegrationTest { - public static async Task ExecuteAllMethods(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) + public async Task Execute(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) { await log.WriteLineAsync("\n***** LISTS *****").ConfigureAwait(false); diff --git a/Source/CakeMail.RestClient.IntegrationTests/Tests/MailingsTests.cs b/Source/CakeMail.RestClient.IntegrationTests/Tests/MailingsTests.cs index b3c86a6..bf3505e 100644 --- a/Source/CakeMail.RestClient.IntegrationTests/Tests/MailingsTests.cs +++ b/Source/CakeMail.RestClient.IntegrationTests/Tests/MailingsTests.cs @@ -1,4 +1,4 @@ -using CakeMail.RestClient.Models; +using CakeMail.RestClient.Models; using System; using System.IO; using System.Linq; @@ -7,9 +7,9 @@ namespace CakeMail.RestClient.IntegrationTests.Tests { - public static class MailingsTests + public class MailingsTests : IIntegrationTest { - public static async Task ExecuteAllMethods(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) + public async Task Execute(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) { await log.WriteLineAsync("\n***** MAILINGS *****").ConfigureAwait(false); diff --git a/Source/CakeMail.RestClient.IntegrationTests/Tests/PermissionsTests.cs b/Source/CakeMail.RestClient.IntegrationTests/Tests/PermissionsTests.cs index 15f3917..54cb364 100644 --- a/Source/CakeMail.RestClient.IntegrationTests/Tests/PermissionsTests.cs +++ b/Source/CakeMail.RestClient.IntegrationTests/Tests/PermissionsTests.cs @@ -6,9 +6,9 @@ namespace CakeMail.RestClient.IntegrationTests.Tests { - public static class PermissionsTests + public class PermissionsTests : IIntegrationTest { - public static async Task ExecuteAllMethods(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) + public async Task Execute(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) { await log.WriteLineAsync("\n***** PERMISSIONS *****").ConfigureAwait(false); diff --git a/Source/CakeMail.RestClient.IntegrationTests/Tests/RelaysTests.cs b/Source/CakeMail.RestClient.IntegrationTests/Tests/RelaysTests.cs index 9e9d28a..2912101 100644 --- a/Source/CakeMail.RestClient.IntegrationTests/Tests/RelaysTests.cs +++ b/Source/CakeMail.RestClient.IntegrationTests/Tests/RelaysTests.cs @@ -5,9 +5,9 @@ namespace CakeMail.RestClient.IntegrationTests.Tests { - public static class RelaysTests + public class RelaysTests : IIntegrationTest { - public static async Task ExecuteAllMethods(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) + public async Task Execute(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) { await log.WriteLineAsync("\n***** RELAYS *****").ConfigureAwait(false); diff --git a/Source/CakeMail.RestClient.IntegrationTests/Tests/SuppressionListsTests.cs b/Source/CakeMail.RestClient.IntegrationTests/Tests/SuppressionListsTests.cs index 1fc0a3f..182697f 100644 --- a/Source/CakeMail.RestClient.IntegrationTests/Tests/SuppressionListsTests.cs +++ b/Source/CakeMail.RestClient.IntegrationTests/Tests/SuppressionListsTests.cs @@ -5,9 +5,9 @@ namespace CakeMail.RestClient.IntegrationTests.Tests { - public static class SuppressionListsTests + public class SuppressionListsTests : IIntegrationTest { - public static async Task ExecuteAllMethods(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) + public async Task Execute(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) { await log.WriteLineAsync("\n***** SUPPRESSION LISTS *****").ConfigureAwait(false); diff --git a/Source/CakeMail.RestClient.IntegrationTests/Tests/TemplatesTests.cs b/Source/CakeMail.RestClient.IntegrationTests/Tests/TemplatesTests.cs index 9f3ad55..328bf94 100644 --- a/Source/CakeMail.RestClient.IntegrationTests/Tests/TemplatesTests.cs +++ b/Source/CakeMail.RestClient.IntegrationTests/Tests/TemplatesTests.cs @@ -6,9 +6,9 @@ namespace CakeMail.RestClient.IntegrationTests.Tests { - public static class TemplatesTests + public class TemplatesTests : IIntegrationTest { - public static async Task ExecuteAllMethods(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) + public async Task Execute(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) { await log.WriteLineAsync("\n***** TEMPLATES *****").ConfigureAwait(false); diff --git a/Source/CakeMail.RestClient.IntegrationTests/Tests/TimezonesTests.cs b/Source/CakeMail.RestClient.IntegrationTests/Tests/TimezonesTests.cs index 5492dae..7ee7a16 100644 --- a/Source/CakeMail.RestClient.IntegrationTests/Tests/TimezonesTests.cs +++ b/Source/CakeMail.RestClient.IntegrationTests/Tests/TimezonesTests.cs @@ -5,9 +5,9 @@ namespace CakeMail.RestClient.IntegrationTests.Tests { - public static class TimezonesTests + public class TimezonesTests : IIntegrationTest { - public static async Task ExecuteAllMethods(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) + public async Task Execute(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) { await log.WriteLineAsync("\n***** TIMEZONES *****").ConfigureAwait(false); diff --git a/Source/CakeMail.RestClient.IntegrationTests/Tests/TriggersTests.cs b/Source/CakeMail.RestClient.IntegrationTests/Tests/TriggersTests.cs index 9375756..bd1ece2 100644 --- a/Source/CakeMail.RestClient.IntegrationTests/Tests/TriggersTests.cs +++ b/Source/CakeMail.RestClient.IntegrationTests/Tests/TriggersTests.cs @@ -6,9 +6,9 @@ namespace CakeMail.RestClient.IntegrationTests.Tests { - public static class TriggersTests + public class TriggersTests : IIntegrationTest { - public static async Task ExecuteAllMethods(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) + public async Task Execute(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) { await log.WriteLineAsync("\n***** TRIGGERS *****").ConfigureAwait(false); @@ -77,7 +77,7 @@ public static async Task ExecuteAllMethods(ICakeMailRestClient client, string us await log.WriteLineAsync($"List deleted: {(listDeleted ? "success" : "failed")}").ConfigureAwait(false); var campaignDeleted = await client.Campaigns.DeleteAsync(userKey, campaignId, clientId, cancellationToken).ConfigureAwait(false); - await log.WriteLineAsync($"List deleted: {(campaignDeleted ? "success" : "failed")}").ConfigureAwait(false); + await log.WriteLineAsync($"Campaign deleted: {(campaignDeleted ? "success" : "failed")}").ConfigureAwait(false); } } } diff --git a/Source/CakeMail.RestClient.IntegrationTests/Tests/UsersTests.cs b/Source/CakeMail.RestClient.IntegrationTests/Tests/UsersTests.cs index b2ad981..24da425 100644 --- a/Source/CakeMail.RestClient.IntegrationTests/Tests/UsersTests.cs +++ b/Source/CakeMail.RestClient.IntegrationTests/Tests/UsersTests.cs @@ -6,9 +6,9 @@ namespace CakeMail.RestClient.IntegrationTests.Tests { - public static class UsersTests + public class UsersTests : IIntegrationTest { - public static async Task ExecuteAllMethods(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) + public async Task Execute(ICakeMailRestClient client, string userKey, long clientId, TextWriter log, CancellationToken cancellationToken) { await log.WriteLineAsync("\n***** USERS *****").ConfigureAwait(false); diff --git a/Source/CakeMail.RestClient.UnitTests/CakeMailRestClientTests.cs b/Source/CakeMail.RestClient.UnitTests/CakeMailRestClientTests.cs index 0bd130b..33e4dba 100644 --- a/Source/CakeMail.RestClient.UnitTests/CakeMailRestClientTests.cs +++ b/Source/CakeMail.RestClient.UnitTests/CakeMailRestClientTests.cs @@ -23,13 +23,25 @@ public class CakeMailRestClientTests public void Version_is_not_empty() { // Arrange - var client = new CakeMailRestClient(API_KEY); // Act - var result = client.Version; + var version = CakeMailRestClient.Version; + + // Assert + version.ShouldNotBeNullOrEmpty(); + } + + [Fact] + public void UserAgent() + { + // Arrange + + // Act + var userAgent = CakeMailRestClient.UserAgent; // Assert - result.ShouldNotBeNullOrEmpty(); + userAgent.Split(new[] { '/' })[0].ShouldBe("CakeMail .NET REST Client"); + userAgent.Split(new[] { '+' })[1].Trim(new[] { '(', ')' }).ShouldBe("https://github.com/Jericho/CakeMail.RestClient"); } [Fact] @@ -39,12 +51,9 @@ public void RestClient_constructor_with_ApiKey() // Act var client = new CakeMailRestClient(API_KEY); - var userAgent = client.UserAgent; var baseUrl = client.BaseUrl; // Assert - userAgent.Split(new[] { '/' })[0].ShouldBe("CakeMail .NET REST Client"); - userAgent.Split(new[] { '+' })[1].Trim(new[] { '(', ')' }).ShouldBe("https://github.com/Jericho/CakeMail.RestClient"); baseUrl.ShouldBe(new Uri("https://api.wbsrvc.com")); } diff --git a/Source/CakeMail.RestClient/CakeMailRestClient.cs b/Source/CakeMail.RestClient/CakeMailRestClient.cs index ff63705..129416a 100644 --- a/Source/CakeMail.RestClient/CakeMailRestClient.cs +++ b/Source/CakeMail.RestClient/CakeMailRestClient.cs @@ -1,3 +1,4 @@ +using CakeMail.RestClient.Logging; using CakeMail.RestClient.Resources; using CakeMail.RestClient.Utilities; using Pathoschild.Http.Client; @@ -29,14 +30,22 @@ public class CakeMailRestClient : ICakeMailRestClient #region PROPERTIES /// - /// Gets the API key provided by CakeMail. + /// Gets the Version. /// - public string ApiKey { get; private set; } + /// + /// The version. + /// + public static string Version { get; private set; } /// /// Gets the user agent. /// - public string UserAgent { get; private set; } + public static string UserAgent { get; private set; } + + /// + /// Gets the API key provided by CakeMail. + /// + public string ApiKey { get; private set; } /// /// Gets the URL where all API requests are sent. @@ -108,18 +117,22 @@ public class CakeMailRestClient : ICakeMailRestClient /// public ITriggers Triggers { get; private set; } - /// - /// Gets the Version. - /// - /// - /// Gets the version. - /// - public string Version { get; private set; } - #endregion #region CONSTRUCTORS AND DESTRUCTORS + /// + /// Initializes static members of the class. + /// + static CakeMailRestClient() + { + Version = typeof(CakeMailRestClient).GetTypeInfo().Assembly.GetName().Version.ToString(3); +#if DEBUG + Version = "DEBUG"; +#endif + UserAgent = $"CakeMail .NET REST Client/{Version} (+https://github.com/Jericho/CakeMail.RestClient)"; + } + /// /// Initializes a new instance of the class. /// @@ -171,19 +184,14 @@ private CakeMailRestClient(string apiKey, HttpClient httpClient, bool disposeCli ApiKey = apiKey; BaseUrl = new Uri(CAKEMAIL_BASE_URI); - Version = typeof(CakeMailRestClient).GetTypeInfo().Assembly.GetName().Version.ToString(3); -#if DEBUG - Version = "DEBUG"; -#endif - UserAgent = $"CakeMail .NET REST Client/{Version} (+https://github.com/Jericho/CakeMail.RestClient)"; _fluentClient = new FluentClient(this.BaseUrl, httpClient) - .SetUserAgent(this.UserAgent); + .SetUserAgent(CakeMailRestClient.UserAgent); _fluentClient.Filters.Remove(); // Order is important: DiagnosticHandler must be first. - _fluentClient.Filters.Add(new DiagnosticHandler(_options.LogBehavior)); + _fluentClient.Filters.Add(new DiagnosticHandler(_options.LogLevelSuccessfulCalls, _options.LogLevelFailedCalls)); _fluentClient.Filters.Add(new CakeMailErrorHandler()); _fluentClient.BaseClient.DefaultRequestHeaders.Add("apikey", this.ApiKey); @@ -277,7 +285,11 @@ private CakeMailClientOptions GetDefaultOptions() { return new CakeMailClientOptions() { - LogBehavior = LogBehavior.LogEverything + // Setting to 'Debug' to mimic previous behavior. I think this is a sensible default setting. + LogLevelSuccessfulCalls = LogLevel.Debug, + + // Setting to 'Debug' to mimic previous behavior. I think 'Error' would make more sense. + LogLevelFailedCalls = LogLevel.Debug }; } diff --git a/Source/CakeMail.RestClient/ICakeMailRestClient.cs b/Source/CakeMail.RestClient/ICakeMailRestClient.cs index 4d760a6..331461c 100644 --- a/Source/CakeMail.RestClient/ICakeMailRestClient.cs +++ b/Source/CakeMail.RestClient/ICakeMailRestClient.cs @@ -1,4 +1,4 @@ -using CakeMail.RestClient.Resources; +using CakeMail.RestClient.Resources; using System; namespace CakeMail.RestClient @@ -13,11 +13,6 @@ public interface ICakeMailRestClient /// string ApiKey { get; } - /// - /// Gets the user agent. - /// - string UserAgent { get; } - /// /// Gets the URL where all API requests are sent. /// @@ -87,13 +82,5 @@ public interface ICakeMailRestClient /// Gets the Triggers resource. /// ITriggers Triggers { get; } - - /// - /// Gets the Version. - /// - /// - /// Gets the version. - /// - string Version { get; } } } diff --git a/Source/CakeMail.RestClient/Properties/AssemblyInfo.cs b/Source/CakeMail.RestClient/Properties/AssemblyInfo.cs index 8d522dc..a4b453c 100644 --- a/Source/CakeMail.RestClient/Properties/AssemblyInfo.cs +++ b/Source/CakeMail.RestClient/Properties/AssemblyInfo.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Runtime.CompilerServices; [assembly: CLSCompliant(true)] [assembly: InternalsVisibleTo("CakeMail.RestClient.UnitTests")] +[assembly: InternalsVisibleTo("CakeMail.RestClient.IntegrationTests")] diff --git a/Source/CakeMail.RestClient/Utilities/CakeMailClientOptions.cs b/Source/CakeMail.RestClient/Utilities/CakeMailClientOptions.cs index 3627ad2..f09362c 100644 --- a/Source/CakeMail.RestClient/Utilities/CakeMailClientOptions.cs +++ b/Source/CakeMail.RestClient/Utilities/CakeMailClientOptions.cs @@ -1,3 +1,5 @@ +using CakeMail.RestClient.Logging; + namespace CakeMail.RestClient.Utilities { /// @@ -6,8 +8,13 @@ namespace CakeMail.RestClient.Utilities public class CakeMailClientOptions { /// - /// Gets or sets the logging behavior. + /// Gets or sets the log levels for successful calls (HTTP status code in the 200-299 range). /// - public LogBehavior LogBehavior { get; set; } + public LogLevel LogLevelSuccessfulCalls { get; set; } + + /// + /// Gets or sets the log levels for failed calls (HTTP status code outside of the 200-299 range). + /// + public LogLevel LogLevelFailedCalls { get; set; } } } diff --git a/Source/CakeMail.RestClient/Utilities/DiagnosticHandler.cs b/Source/CakeMail.RestClient/Utilities/DiagnosticHandler.cs index 6a67369..3a61349 100644 --- a/Source/CakeMail.RestClient/Utilities/DiagnosticHandler.cs +++ b/Source/CakeMail.RestClient/Utilities/DiagnosticHandler.cs @@ -2,7 +2,7 @@ using Pathoschild.Http.Client; using Pathoschild.Http.Client.Extensibility; using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Diagnostics; using System.Linq; using System.Net.Http; @@ -21,21 +21,23 @@ internal class DiagnosticHandler : IHttpFilter internal const string DIAGNOSTIC_ID_HEADER_NAME = "CakeMailRestClient-Diagnostic-Id"; private static readonly ILog _logger = LogProvider.For(); - private readonly LogBehavior _logBehavior; + private readonly LogLevel _logLevelSuccessfulCalls; + private readonly LogLevel _logLevelFailedCalls; #endregion #region PROPERTIES - internal static IDictionary RequestReference, StringBuilder Diagnostic, long RequestTimestamp, long ResponseTimeStamp)> DiagnosticsInfo { get; } = new Dictionary, StringBuilder, long, long)>(); + internal static ConcurrentDictionary RequestReference, string Diagnostic, long RequestTimestamp, long ResponseTimeStamp)> DiagnosticsInfo { get; } = new ConcurrentDictionary, string, long, long)>(); #endregion #region CTOR - public DiagnosticHandler(LogBehavior logBehavior) + public DiagnosticHandler(LogLevel logLevelSuccessfulCalls, LogLevel logLevelFailedCalls) { - _logBehavior = logBehavior; + _logLevelSuccessfulCalls = logLevelSuccessfulCalls; + _logLevelFailedCalls = logLevelFailedCalls; } #endregion @@ -60,10 +62,7 @@ public void OnRequest(IRequest request) LogContent(diagnostic, httpRequest.Content); // Add the diagnotic info to our cache - lock (DiagnosticsInfo) - { - DiagnosticsInfo.Add(diagnosticId, (new WeakReference(request.Message), diagnostic, Stopwatch.GetTimestamp(), long.MinValue)); - } + DiagnosticsInfo.TryAdd(diagnosticId, (new WeakReference(request.Message), diagnostic.ToString(), Stopwatch.GetTimestamp(), long.MinValue)); } /// Method invoked just after the HTTP response is received. This method can modify the incoming HTTP response. @@ -74,63 +73,52 @@ public void OnResponse(IResponse response, bool httpErrorAsException) var responseTimestamp = Stopwatch.GetTimestamp(); var httpResponse = response.Message; - var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DiagnosticHandler.DIAGNOSTIC_ID_HEADER_NAME); - var diagnosticInfo = DiagnosticsInfo[diagnosticId]; - diagnosticInfo.ResponseTimeStamp = responseTimestamp; - - try + var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DIAGNOSTIC_ID_HEADER_NAME); + if (DiagnosticsInfo.TryGetValue(diagnosticId, out (WeakReference RequestReference, string Diagnostic, long RequestTimestamp, long ResponseTimestamp) diagnosticInfo)) { - // Log the response - diagnosticInfo.Diagnostic.AppendLine(); - diagnosticInfo.Diagnostic.AppendLine("RESPONSE:"); - diagnosticInfo.Diagnostic.AppendLine($" HTTP/{httpResponse.Version} {(int)httpResponse.StatusCode} {httpResponse.ReasonPhrase}"); - LogHeaders(diagnosticInfo.Diagnostic, httpResponse.Headers); - LogContent(diagnosticInfo.Diagnostic, httpResponse.Content); - - // Calculate how much time elapsed between request and response - var elapsed = TimeSpan.FromTicks(diagnosticInfo.ResponseTimeStamp - diagnosticInfo.RequestTimestamp); - - // Log diagnostic - diagnosticInfo.Diagnostic.AppendLine(); - diagnosticInfo.Diagnostic.AppendLine("DIAGNOSTIC:"); - diagnosticInfo.Diagnostic.AppendLine($" The request took {elapsed.ToDurationString()}"); - } - catch (Exception e) - { - Debug.WriteLine("{0}\r\nAN EXCEPTION OCCURRED: {1}\r\n{0}", new string('=', 50), e.GetBaseException().Message); - diagnosticInfo.Diagnostic.AppendLine($"AN EXCEPTION OCCURRED: {e.GetBaseException().Message}"); - - if (_logger != null && _logger.IsErrorEnabled()) + var updatedDiagnostic = new StringBuilder(diagnosticInfo.Diagnostic); + try { - _logger.Error(e, "An exception occurred when inspecting the response from CakeMail"); + // Log the response + updatedDiagnostic.AppendLine(); + updatedDiagnostic.AppendLine("RESPONSE:"); + updatedDiagnostic.AppendLine($" HTTP/{httpResponse.Version} {(int)httpResponse.StatusCode} {httpResponse.ReasonPhrase}"); + LogHeaders(updatedDiagnostic, httpResponse.Headers); + LogContent(updatedDiagnostic, httpResponse.Content); + + // Calculate how much time elapsed between request and response + var elapsed = TimeSpan.FromTicks(responseTimestamp - diagnosticInfo.RequestTimestamp); + + // Log diagnostic + updatedDiagnostic.AppendLine(); + updatedDiagnostic.AppendLine("DIAGNOSTIC:"); + updatedDiagnostic.AppendLine($" The request took {elapsed.ToDurationString()}"); } - } - finally - { - var diagnosticMessage = diagnosticInfo.Diagnostic.ToString(); - - if (!string.IsNullOrEmpty(diagnosticMessage)) + catch (Exception e) { - Debug.WriteLine("{0}\r\n{1}{0}", new string('=', 50), diagnosticMessage); + Debug.WriteLine("{0}\r\nAN EXCEPTION OCCURRED: {1}\r\n{0}", new string('=', 50), e.GetBaseException().Message); + updatedDiagnostic.AppendLine($"AN EXCEPTION OCCURRED: {e.GetBaseException().Message}"); - if (_logger != null && _logger.IsDebugEnabled()) + if (_logger != null && _logger.IsErrorEnabled()) { - var shouldLog = response.IsSuccessStatusCode && _logBehavior.HasFlag(LogBehavior.LogSuccessfulCalls); - shouldLog |= !response.IsSuccessStatusCode && _logBehavior.HasFlag(LogBehavior.LogFailedCalls); - - if (shouldLog) - { - _logger.Debug(diagnosticMessage - .Replace("{", "{{") - .Replace("}", "}}")); - } + _logger.Error(e, "An exception occurred when inspecting the response from SendGrid"); } } + finally + { + var diagnosticMessage = updatedDiagnostic.ToString(); - DiagnosticsInfo[diagnosticId] = diagnosticInfo; + LogDiagnostic(response.IsSuccessStatusCode, _logLevelSuccessfulCalls, diagnosticMessage); + LogDiagnostic(!response.IsSuccessStatusCode, _logLevelFailedCalls, diagnosticMessage); - Cleanup(); + DiagnosticsInfo.TryUpdate( + diagnosticId, + (diagnosticInfo.RequestReference, updatedDiagnostic.ToString(), diagnosticInfo.RequestTimestamp, responseTimestamp), + (diagnosticInfo.RequestReference, diagnosticInfo.Diagnostic, diagnosticInfo.RequestTimestamp, diagnosticInfo.ResponseTimestamp)); + } } + + Cleanup(); } #endregion @@ -179,6 +167,20 @@ private void LogContent(StringBuilder diagnostic, HttpContent httpContent) } } + private void LogDiagnostic(bool shouldLog, LogLevel logLEvel, string diagnosticMessage) + { + if (shouldLog && _logger != null) + { + var logLevelEnabled = _logger.Log(logLEvel, null, null, Array.Empty()); + if (logLevelEnabled) + { + _logger.Log(logLEvel, () => diagnosticMessage + .Replace("{", "{{") + .Replace("}", "}}")); + } + } + } + private void Cleanup() { try @@ -186,11 +188,12 @@ private void Cleanup() // Remove diagnostic information for requests that have been garbage collected foreach (string key in DiagnosticHandler.DiagnosticsInfo.Keys.ToArray()) { - var diagnosticInfo = DiagnosticHandler.DiagnosticsInfo[key]; - if (!diagnosticInfo.RequestReference.TryGetTarget(out HttpRequestMessage request)) + if (DiagnosticHandler.DiagnosticsInfo.TryGetValue(key, out (WeakReference RequestReference, string Diagnostic, long RequestTimeStamp, long ResponseTimestamp) diagnosticInfo)) { - DiagnosticHandler.DiagnosticsInfo.Remove(key); - continue; + if (!diagnosticInfo.RequestReference.TryGetTarget(out HttpRequestMessage request)) + { + DiagnosticsInfo.TryRemove(key, out _); + } } } } diff --git a/Source/CakeMail.RestClient/Utilities/ExtensionMethods.cs b/Source/CakeMail.RestClient/Utilities/ExtensionMethods.cs index 0499f4a..fd494f4 100644 --- a/Source/CakeMail.RestClient/Utilities/ExtensionMethods.cs +++ b/Source/CakeMail.RestClient/Utilities/ExtensionMethods.cs @@ -10,6 +10,7 @@ using System.Reflection; using System.Runtime.Serialization; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace CakeMail.RestClient.Utilities @@ -265,6 +266,55 @@ public static Encoding GetEncoding(this HttpContent content, Encoding defaultEnc return encoding; } + public static async Task ForEachAsync(this IEnumerable items, Func> action, int maxDegreeOfParalellism) + { + var allTasks = new List>(); + var throttler = new SemaphoreSlim(initialCount: maxDegreeOfParalellism); + foreach (var item in items) + { + await throttler.WaitAsync(); + allTasks.Add( + Task.Run(async () => + { + try + { + return await action(item).ConfigureAwait(false); + } + finally + { + throttler.Release(); + } + })); + } + + var results = await Task.WhenAll(allTasks).ConfigureAwait(false); + return results; + } + + public static async Task ForEachAsync(this IEnumerable items, Func action, int maxDegreeOfParalellism) + { + var allTasks = new List(); + var throttler = new SemaphoreSlim(initialCount: maxDegreeOfParalellism); + foreach (var item in items) + { + await throttler.WaitAsync(); + allTasks.Add( + Task.Run(async () => + { + try + { + await action(item).ConfigureAwait(false); + } + finally + { + throttler.Release(); + } + })); + } + + await Task.WhenAll(allTasks).ConfigureAwait(false); + } + /// Asynchronously parses the JSON response from the CakeMail API and converts the data the desired type. /// The response model to deserialize into. /// The content. diff --git a/Source/CakeMail.RestClient/Utilities/LogBehavior.cs b/Source/CakeMail.RestClient/Utilities/LogBehavior.cs deleted file mode 100644 index d22c432..0000000 --- a/Source/CakeMail.RestClient/Utilities/LogBehavior.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; - -namespace CakeMail.RestClient.Utilities -{ - /// - /// Logging behavior. - /// Allows you to decide if only successful calls, only failed calls, both or neither should be logged. - /// - [Flags] - public enum LogBehavior : short - { - /// - /// Do not log any calls. - /// - LogNothing = 0, - - /// - /// Log successful calls (i.e.: calls with a response StatusCode in the 200-299 range). - /// - LogSuccessfulCalls = 1 << 0, - - /// - /// Log failed calls. - /// - LogFailedCalls = 1 << 1, - - /// - /// Log all calls. - /// - LogEverything = LogSuccessfulCalls | LogFailedCalls - } -}