From 7efc49596e0a7313bdaddac0967f933bc3da00ae Mon Sep 17 00:00:00 2001 From: Denis-Cosmin Nutiu Date: Mon, 20 Apr 2020 18:19:32 +0300 Subject: [PATCH] NUC-42: Write partial unit tests for TelemetryPublisherFirestore --- NucuCar.Common/HttpClient.cs | 4 +- .../Telemetry/TelemetryPublisher.cs | 2 +- .../TelemetryPublisherFirestore.cs | 15 +- .../TelemetryPublisherType.cs | 4 +- .../ConnectionStringParserTest.cs | 120 ++++++------- .../TelemetryPublisherFirestoreTest.cs | 7 - .../TelemetryPublisherFactoryTest.cs | 13 +- .../TelemetryPublisherFirestoreTest.cs | 164 ++++++++++++++++++ NucuCar.sln.DotSettings.user | 42 ++++- 9 files changed, 292 insertions(+), 79 deletions(-) rename {NucuCar.Domain/Telemetry => NucuCar.Telemetry}/TelemetryPublisherType.cs (84%) rename NucuCar.UnitTests/{NucuCar.Domain.Tests/Utilities => NucuCar.Common.Tests}/ConnectionStringParserTest.cs (94%) delete mode 100644 NucuCar.UnitTests/NucuCar.Domain.Tests/Telemetry/TelemetryPublisherFirestoreTest.cs rename NucuCar.UnitTests/{NucuCar.Domain.Tests/Telemetry => NucuCar.Telemetry.Tests}/TelemetryPublisherFactoryTest.cs (71%) create mode 100644 NucuCar.UnitTests/NucuCar.Telemetry.Tests/TelemetryPublisherFirestoreTest.cs diff --git a/NucuCar.Common/HttpClient.cs b/NucuCar.Common/HttpClient.cs index b123951..1520c12 100644 --- a/NucuCar.Common/HttpClient.cs +++ b/NucuCar.Common/HttpClient.cs @@ -186,10 +186,12 @@ namespace NucuCar.Common /// private sNetHttp.HttpRequestMessage _makeRequest(sNetHttp.HttpMethod method, string path) { + var uri = _httpClient.BaseAddress == null ? new Uri(path) : new Uri(_httpClient.BaseAddress, path); + var requestMessage = new sNetHttp.HttpRequestMessage { Method = method, - RequestUri = new Uri(_httpClient.BaseAddress, path) + RequestUri = uri }; requestMessage.Headers.Authorization = _httpClient.DefaultRequestHeaders.Authorization; diff --git a/NucuCar.Domain/Telemetry/TelemetryPublisher.cs b/NucuCar.Domain/Telemetry/TelemetryPublisher.cs index 86f4644..0220e43 100644 --- a/NucuCar.Domain/Telemetry/TelemetryPublisher.cs +++ b/NucuCar.Domain/Telemetry/TelemetryPublisher.cs @@ -96,7 +96,7 @@ namespace NucuCar.Domain.Telemetry /// It also adds metadata information such as: source and timestamp. /// /// A dictionary containing all telemetry data. - protected Dictionary GetTelemetry() + protected virtual Dictionary GetTelemetry() { var data = new List>(); foreach (var telemeter in RegisteredTelemeters) diff --git a/NucuCar.Telemetry/TelemetryPublisherFirestore.cs b/NucuCar.Telemetry/TelemetryPublisherFirestore.cs index 4b4e6c2..a981325 100644 --- a/NucuCar.Telemetry/TelemetryPublisherFirestore.cs +++ b/NucuCar.Telemetry/TelemetryPublisherFirestore.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Net; using System.Threading; @@ -26,7 +27,7 @@ namespace NucuCar.Telemetry /// public class TelemetryPublisherFirestore : TelemetryPublisher { - private readonly HttpClient _httpClient; + protected HttpClient HttpClient; private string _idToken; @@ -44,6 +45,7 @@ namespace NucuCar.Telemetry Logger?.LogCritical( $"Can't start {nameof(TelemetryPublisherFirestore)}! Malformed connection string! " + $"Missing ProjectId!"); + throw new ArgumentException("Malformed connection string!"); } if (!options.TryGetValue("CollectionName", out var firestoreCollection)) @@ -51,6 +53,7 @@ namespace NucuCar.Telemetry Logger?.LogCritical( $"Can't start {nameof(TelemetryPublisherFirestore)}! Malformed connection string! " + $"Missing CollectionName!"); + throw new ArgumentException("Malformed connection string!"); } var timeout = int.Parse(options.GetValueOrDefault("Timeout", "10000") ?? "10000"); @@ -61,7 +64,7 @@ namespace NucuCar.Telemetry // Setup HttpClient var requestUrl = $"https://firestore.googleapis.com/v1/projects/{firestoreProjectId}/" + $"databases/(default)/documents/{firestoreCollection}/"; - _httpClient = new HttpClient(requestUrl) {Timeout = timeout, Logger = Logger}; + HttpClient = new HttpClient(requestUrl) {Timeout = timeout, Logger = Logger}; Logger?.LogInformation($"Initialized {nameof(TelemetryPublisherFirestore)}"); Logger?.LogInformation($"ProjectId: {firestoreProjectId}; CollectionName: {firestoreCollection}."); } @@ -77,13 +80,13 @@ namespace NucuCar.Telemetry ["returnSecureToken"] = true }; - var response = await _httpClient.PostAsync(requestUrl, data); + var response = await HttpClient.PostAsync(requestUrl, data); if (response?.StatusCode == HttpStatusCode.OK) { var jsonContent = await response.GetJson(); _idToken = jsonContent.GetProperty("idToken").ToString(); - _httpClient.Authorization(_idToken); + HttpClient.Authorization(_idToken); } else { @@ -100,7 +103,7 @@ namespace NucuCar.Telemetry } var data = FirebaseRestTranslator.Translator.Translate(null, GetTelemetry()); - var responseMessage = await _httpClient.PostAsync("", data); + var responseMessage = await HttpClient.PostAsync("", data); switch (responseMessage?.StatusCode) { @@ -111,7 +114,7 @@ namespace NucuCar.Telemetry { Logger?.LogError($"Failed to publish telemetry data! {responseMessage.StatusCode}. Retrying..."); await SetupAuthorization(); - responseMessage = await _httpClient.PostAsync("", data); + responseMessage = await HttpClient.PostAsync("", data); if (responseMessage != null && responseMessage.IsSuccessStatusCode) { Logger?.LogInformation("Published data to Firestore on retry!"); diff --git a/NucuCar.Domain/Telemetry/TelemetryPublisherType.cs b/NucuCar.Telemetry/TelemetryPublisherType.cs similarity index 84% rename from NucuCar.Domain/Telemetry/TelemetryPublisherType.cs rename to NucuCar.Telemetry/TelemetryPublisherType.cs index fdc8f52..a49a219 100644 --- a/NucuCar.Domain/Telemetry/TelemetryPublisherType.cs +++ b/NucuCar.Telemetry/TelemetryPublisherType.cs @@ -1,4 +1,6 @@ -namespace NucuCar.Domain.Telemetry +using NucuCar.Domain.Telemetry; + +namespace NucuCar.Telemetry { /// /// TelemetryPublisherType holds constants for instantiating , diff --git a/NucuCar.UnitTests/NucuCar.Domain.Tests/Utilities/ConnectionStringParserTest.cs b/NucuCar.UnitTests/NucuCar.Common.Tests/ConnectionStringParserTest.cs similarity index 94% rename from NucuCar.UnitTests/NucuCar.Domain.Tests/Utilities/ConnectionStringParserTest.cs rename to NucuCar.UnitTests/NucuCar.Common.Tests/ConnectionStringParserTest.cs index 7858881..c614e4f 100644 --- a/NucuCar.UnitTests/NucuCar.Domain.Tests/Utilities/ConnectionStringParserTest.cs +++ b/NucuCar.UnitTests/NucuCar.Common.Tests/ConnectionStringParserTest.cs @@ -1,61 +1,61 @@ -using System; -using System.Collections.Generic; -using NucuCar.Common.Utilities; -using Xunit; - -namespace NucuCar.UnitTests.NucuCar.Domain.Tests.Utilities -{ - public class ConnectionStringParserTest - { - [Fact] - private void Test_ConnectionStringParser_Valid() - { - const string connectionString = "Test=1;Test2=2"; - var parsedString = ConnectionStringParser.Parse(connectionString); - - Assert.Equal("1", parsedString.GetValueOrDefault("Test")); - Assert.Equal("2", parsedString.GetValueOrDefault("Test2")); - } - - [Fact] - private void Test_ConnectionStringParser_EmptyValue() - { - const string connectionString = "Test=1;Test2="; - var parsedString = ConnectionStringParser.Parse(connectionString); - - Assert.Equal("1", parsedString.GetValueOrDefault("Test")); - Assert.Equal(string.Empty, parsedString.GetValueOrDefault("Test2")); - } - - [Fact] - private void Test_ConnectionStringParser_EmptyValue2() - { - Assert.Throws(() => - { - ConnectionStringParser.Parse(string.Empty); - }); - } - - [Fact] - private void Test_ConnectionStringParser_Invalid() - { - const string connectionString = "Test=1;Test2=;d"; - Assert.Throws(() => - { - ConnectionStringParser.Parse(connectionString); - }); - } - - [Fact] - private void Test_ConnectionStringParser_ValueWithMultipleEquals() - { - const string connectionString = "Test=1;Test2=base64="; - var parsedString = ConnectionStringParser.Parse(connectionString); - - Assert.Equal("1", parsedString.GetValueOrDefault("Test")); - Assert.Equal("base64=", parsedString.GetValueOrDefault("Test2")); - } - - - } +using System; +using System.Collections.Generic; +using NucuCar.Common.Utilities; +using Xunit; + +namespace NucuCar.UnitTests.NucuCar.Common.Tests +{ + public class ConnectionStringParserTest + { + [Fact] + private void Test_ConnectionStringParser_Valid() + { + const string connectionString = "Test=1;Test2=2"; + var parsedString = ConnectionStringParser.Parse(connectionString); + + Assert.Equal("1", parsedString.GetValueOrDefault("Test")); + Assert.Equal("2", parsedString.GetValueOrDefault("Test2")); + } + + [Fact] + private void Test_ConnectionStringParser_EmptyValue() + { + const string connectionString = "Test=1;Test2="; + var parsedString = ConnectionStringParser.Parse(connectionString); + + Assert.Equal("1", parsedString.GetValueOrDefault("Test")); + Assert.Equal(string.Empty, parsedString.GetValueOrDefault("Test2")); + } + + [Fact] + private void Test_ConnectionStringParser_EmptyValue2() + { + Assert.Throws(() => + { + ConnectionStringParser.Parse(string.Empty); + }); + } + + [Fact] + private void Test_ConnectionStringParser_Invalid() + { + const string connectionString = "Test=1;Test2=;d"; + Assert.Throws(() => + { + ConnectionStringParser.Parse(connectionString); + }); + } + + [Fact] + private void Test_ConnectionStringParser_ValueWithMultipleEquals() + { + const string connectionString = "Test=1;Test2=base64="; + var parsedString = ConnectionStringParser.Parse(connectionString); + + Assert.Equal("1", parsedString.GetValueOrDefault("Test")); + Assert.Equal("base64=", parsedString.GetValueOrDefault("Test2")); + } + + + } } \ No newline at end of file diff --git a/NucuCar.UnitTests/NucuCar.Domain.Tests/Telemetry/TelemetryPublisherFirestoreTest.cs b/NucuCar.UnitTests/NucuCar.Domain.Tests/Telemetry/TelemetryPublisherFirestoreTest.cs deleted file mode 100644 index ef69901..0000000 --- a/NucuCar.UnitTests/NucuCar.Domain.Tests/Telemetry/TelemetryPublisherFirestoreTest.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NucuCar.UnitTests.NucuCar.Domain.Tests.Telemetry -{ - public class TelemetryPublisherFirestoreTest - { - // TODO after refactoring - } -} \ No newline at end of file diff --git a/NucuCar.UnitTests/NucuCar.Domain.Tests/Telemetry/TelemetryPublisherFactoryTest.cs b/NucuCar.UnitTests/NucuCar.Telemetry.Tests/TelemetryPublisherFactoryTest.cs similarity index 71% rename from NucuCar.UnitTests/NucuCar.Domain.Tests/Telemetry/TelemetryPublisherFactoryTest.cs rename to NucuCar.UnitTests/NucuCar.Telemetry.Tests/TelemetryPublisherFactoryTest.cs index 17afc64..572f2b4 100644 --- a/NucuCar.UnitTests/NucuCar.Domain.Tests/Telemetry/TelemetryPublisherFactoryTest.cs +++ b/NucuCar.UnitTests/NucuCar.Telemetry.Tests/TelemetryPublisherFactoryTest.cs @@ -1,9 +1,8 @@ using System; -using NucuCar.Domain.Telemetry; using NucuCar.Telemetry; using Xunit; -namespace NucuCar.UnitTests.NucuCar.Domain.Tests.Telemetry +namespace NucuCar.UnitTests.NucuCar.Telemetry.Tests { public class TelemetryPublisherFactoryTest { @@ -26,6 +25,16 @@ namespace NucuCar.UnitTests.NucuCar.Domain.Tests.Telemetry TelemetryPublisherFactory.CreateFromConnectionString(TelemetryPublisherType.Disk, connectionString); Assert.IsType(telemetryPublisher); } + + [Fact] + private void Test_Build_TelemetryPublisherFiresstore() + { + const string connectionString = + "ProjectId=test;CollectionName=test"; + var telemetryPublisher = + TelemetryPublisherFactory.CreateFromConnectionString(TelemetryPublisherType.Firestore, connectionString); + Assert.IsType(telemetryPublisher); + } [Fact] private void Test_Build_ThrowsOnInvalidType() diff --git a/NucuCar.UnitTests/NucuCar.Telemetry.Tests/TelemetryPublisherFirestoreTest.cs b/NucuCar.UnitTests/NucuCar.Telemetry.Tests/TelemetryPublisherFirestoreTest.cs new file mode 100644 index 0000000..784187b --- /dev/null +++ b/NucuCar.UnitTests/NucuCar.Telemetry.Tests/TelemetryPublisherFirestoreTest.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NucuCar.Domain.Telemetry; +using NucuCar.Telemetry; +using Xunit; +using HttpClient = NucuCar.Common.HttpClient; + +namespace NucuCar.UnitTests.NucuCar.Telemetry.Tests +{ + /// + /// Class used to test the TelemetryPublisherFirestore by mocking the GetTelemetry method and HttpClient field. + /// + internal class MockTelemetryPublisherFirestore : TelemetryPublisherFirestore + { + private Dictionary _mockData; + + public MockTelemetryPublisherFirestore(TelemetryPublisherBuilderOptions opts) : base(opts) + { + _mockData = new Dictionary(); + } + + public void SetHttpClient(HttpClient client) + { + HttpClient = client; + } + + public void SetMockData(Dictionary data) + { + _mockData = data; + } + + protected override Dictionary GetTelemetry() + { + return _mockData; + } + } + + public class TelemetryPublisherFirestoreTest + { + [Fact] + private void Test_Construct_BadProjectId() + { + // Setup + var opts = new TelemetryPublisherBuilderOptions() + { + ConnectionString = "ProjectIdBAD=test;CollectionName=test" + }; + + // Run & Assert + Assert.Throws(() => { new MockTelemetryPublisherFirestore(opts); }); + } + + [Fact] + private void Test_Construct_BadCollectiontName() + { + // Setup + var opts = new TelemetryPublisherBuilderOptions() + { + ConnectionString = "ProjectId=test;CollectionNameBAD=test" + }; + + // Run & Assert + Assert.Throws(() => { new MockTelemetryPublisherFirestore(opts); }); + } + + [Fact] + private async Task Test_PublishAsync_OK() + { + // Setup + var opts = new TelemetryPublisherBuilderOptions() + { + ConnectionString = "ProjectId=test;CollectionName=test" + }; + var publisher = new MockTelemetryPublisherFirestore(opts); + var mockHttpClient = new Mock("http://testing.com"); + mockHttpClient.Setup(c => c.SendAsync(It.IsAny())) + .Returns(Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))); + + publisher.SetHttpClient(mockHttpClient.Object); + publisher.SetMockData(new Dictionary {["testData"] = 1}); + + // Run + await publisher.PublishAsync(CancellationToken.None); + + // Assert + var expectedContent = "{\"fields\":{\"testData\":{\"integerValue\":1}}}"; + mockHttpClient.Verify( + m => m.SendAsync( + It.Is( + request => request.Method.Equals(HttpMethod.Post)))); + mockHttpClient.Verify( + m => m.SendAsync( + It.Is( + request => request.RequestUri.Equals(new Uri("http://testing.com"))))); + mockHttpClient.Verify( + m => m.SendAsync( + It.Is( + request => request.Content.ReadAsStringAsync().GetAwaiter().GetResult() + .Equals(expectedContent)))); + } + + [Fact] + private async Task Test_PublishAsync_Cancel() + { + // Setup + var opts = new TelemetryPublisherBuilderOptions() + { + ConnectionString = "ProjectId=test;CollectionName=test" + }; + var publisher = new MockTelemetryPublisherFirestore(opts); + var mockHttpClient = new Mock("http://testing.com"); + mockHttpClient.Setup(c => c.SendAsync(It.IsAny())) + .Returns(Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))); + + publisher.SetHttpClient(mockHttpClient.Object); + publisher.SetMockData(new Dictionary {["testData"] = 1}); + var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Run + await publisher.PublishAsync(cts.Token); + + // Assert + mockHttpClient.Verify(m => m.SendAsync(It.IsAny()), Times.Never()); + } + + [Fact] + private async Task Test_PublishAsync_Authorization_OK() + { + // Setup + var sendAsyncInvocations = new List(); + + var opts = new TelemetryPublisherBuilderOptions() + { + ConnectionString = "ProjectId=test;CollectionName=test" + }; + var publisher = new MockTelemetryPublisherFirestore(opts); + var mockHttpClient = new Mock("http://testing.com"); + mockHttpClient.SetupSequence(c => c.SendAsync(It.IsAny())) + .Returns(Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden))) + .Returns(Task.FromResult( + new HttpResponseMessage(HttpStatusCode.OK) + {Content = new StringContent("{\"idToken\":\"testauthtoken\"}")} + )) + .Returns(Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))); + + + publisher.SetHttpClient(mockHttpClient.Object); + publisher.SetMockData(new Dictionary {["testData"] = 1}); + + // Run + await publisher.PublishAsync(CancellationToken.None); + + // Assert + // Can't verify because moq doesn't support that, damn C#. + } + } +} \ No newline at end of file diff --git a/NucuCar.sln.DotSettings.user b/NucuCar.sln.DotSettings.user index a20925d..f13c6ee 100644 --- a/NucuCar.sln.DotSettings.user +++ b/NucuCar.sln.DotSettings.user @@ -2,4 +2,44 @@ <AssemblyExplorer> <Assembly Path="/home/denis/.nuget/packages/iot.device.bindings/1.0.0/lib/netcoreapp2.1/Iot.Device.Bindings.dll" /> <Assembly Path="/home/denis/.nuget/packages/firebaseresttranslator/0.1.1/lib/netcoreapp3.0/FirebaseRestTranslator.dll" /> -</AssemblyExplorer> \ No newline at end of file +</AssemblyExplorer> + <SessionState ContinuousTestingIsOn="False" ContinuousTestingMode="0" FrameworkVersion="{x:Null}" IsLocked="False" Name="Test_PublishAsync_Authorization_OK" PlatformMonoPreference="{x:Null}" PlatformType="{x:Null}" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + <TestAncestor> + <TestId>xUnit::C6F07921-1052-4945-911E-F328A622F229::.NETCoreApp,Version=v3.0::NucuCar.UnitTests.NucuCar.Telemetry.Tests.TelemetryPublisherFirestoreTest.Test_PublishAsync_Authorization_OK</TestId> + </TestAncestor> +</SessionState> + <SessionState ContinuousTestingIsOn="False" ContinuousTestingMode="0" FrameworkVersion="{x:Null}" IsLocked="False" Name="Session" PlatformMonoPreference="{x:Null}" PlatformType="{x:Null}" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + <Or> + <Or> + <Or> + <Or> + <Or> + <Or> + <TestAncestor> + <TestId>xUnit::C6F07921-1052-4945-911E-F328A622F229::.NETCoreApp,Version=v3.0::NucuCar.UnitTests.NucuCar.Telemetry.Tests.TelemetryPublisherFirestoreTest.Test_PublishAsync_OK</TestId> + </TestAncestor> + <TestAncestor> + <TestId>xUnit::C6F07921-1052-4945-911E-F328A622F229::.NETCoreApp,Version=v3.0::NucuCar.UnitTests.NucuCar.Telemetry.Tests.TelemetryPublisherFirestoreTest.Test_PublishAsync_BadProjectId</TestId> + </TestAncestor> + </Or> + <TestAncestor> + <TestId>xUnit::C6F07921-1052-4945-911E-F328A622F229::.NETCoreApp,Version=v3.0::NucuCar.UnitTests.NucuCar.Telemetry.Tests.TelemetryPublisherFirestoreTest.Test_PublishAsync_CollectiontName</TestId> + </TestAncestor> + </Or> + <TestAncestor> + <TestId>xUnit::C6F07921-1052-4945-911E-F328A622F229::.NETCoreApp,Version=v3.0::NucuCar.UnitTests.NucuCar.Telemetry.Tests.TelemetryPublisherFirestoreTest.Test_PublishAsync_Error_Timeout</TestId> + </TestAncestor> + </Or> + <TestAncestor> + <TestId>xUnit::C6F07921-1052-4945-911E-F328A622F229::.NETCoreApp,Version=v3.0::NucuCar.UnitTests.NucuCar.Telemetry.Tests.TelemetryPublisherFirestoreTest.Test_PublishAsync_Cancel</TestId> + </TestAncestor> + </Or> + <TestAncestor> + <TestId>xUnit::C6F07921-1052-4945-911E-F328A622F229::.NETCoreApp,Version=v3.0::NucuCar.UnitTests.NucuCar.Telemetry.Tests.TelemetryPublisherFirestoreTest.Test_PublishAsync_UnknownError</TestId> + </TestAncestor> + </Or> + <TestAncestor> + <TestId>xUnit::C6F07921-1052-4945-911E-F328A622F229::.NETCoreApp,Version=v3.0::NucuCar.UnitTests.NucuCar.Telemetry.Tests.TelemetryPublisherFirestoreTest.Test_PublishAsync_AuthorizationFail</TestId> + </TestAncestor> + </Or> +</SessionState> \ No newline at end of file