From b879cbb16656b095fdd6f9a2838c0ca496dbe043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomas=20Hellstr=C3=B6m?= Date: Mon, 7 Sep 2020 09:52:12 +0200 Subject: [PATCH 1/5] Added possibility to test (#200) * Added possibility to test * Fix * Default not specify timeout value * typo --- .github/workflows/tags_nuget.yml | 2 + NetDaemon.sln | 17 ++ .../Reactive/ObservableExtensionMethods.cs | 20 ++ .../NetDaemon.Daemon/Daemon/NetDaemonHost.cs | 1 + tests/NetDaemon.Test/DaemonHostTestBase.cs | 241 +++++++++++++++++ tests/NetDaemon.Test/HassClientMock.cs | 245 ++++++++++++++++++ tests/NetDaemon.Test/HttpHandlerMock.cs | 74 ++++++ tests/NetDaemon.Test/LoggerMock.cs | 69 +++++ tests/NetDaemon.Test/NetDaemon.Test.csproj | 38 +++ tests/NetDaemon.Test/Tests/RxApp.cs | 22 ++ .../NetDaemon.Test/Tests/TestTheTestClass.cs | 21 ++ 11 files changed, 750 insertions(+) create mode 100644 tests/NetDaemon.Test/DaemonHostTestBase.cs create mode 100644 tests/NetDaemon.Test/HassClientMock.cs create mode 100644 tests/NetDaemon.Test/HttpHandlerMock.cs create mode 100644 tests/NetDaemon.Test/LoggerMock.cs create mode 100644 tests/NetDaemon.Test/NetDaemon.Test.csproj create mode 100644 tests/NetDaemon.Test/Tests/RxApp.cs create mode 100644 tests/NetDaemon.Test/Tests/TestTheTestClass.cs diff --git a/.github/workflows/tags_nuget.yml b/.github/workflows/tags_nuget.yml index fc9c8a5ea..fd873dc2a 100644 --- a/.github/workflows/tags_nuget.yml +++ b/.github/workflows/tags_nuget.yml @@ -23,5 +23,7 @@ jobs: run: dotnet pack src/App/NetDaemon.App/NetDaemon.App.csproj --configuration Release -p:PackageVersion=${GIT_TAG_NAME}-beta - name: pack Daemon run: dotnet pack src/Daemon/NetDaemon.Daemon/NetDaemon.Daemon.csproj --configuration Release -p:PackageVersion=${GIT_TAG_NAME}-beta + - name: pack Test + run: dotnet pack tests/NetDaemon.Test/NetDaemon.Test.csproj --configuration Release -p:PackageVersion=${GIT_TAG_NAME}-beta - name: push to nuget run: dotnet nuget push **/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json --no-symbols true diff --git a/NetDaemon.sln b/NetDaemon.sln index 9a8abf6e7..67937bb07 100644 --- a/NetDaemon.sln +++ b/NetDaemon.sln @@ -24,6 +24,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DaemonRunner", "src\DaemonR EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DaemonRunner", "DaemonRunner", "{A7F8279D-A148-48B8-8492-BDD96CE1E676}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{32E20E48-2F62-447B-83A2-DB2EA7F61AB3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetDaemon.Test", "tests\NetDaemon.Test\NetDaemon.Test.csproj", "{E1844E4F-2711-4A04-B70C-D67D5D2C8F31}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -94,6 +98,18 @@ Global {E79D336A-0BA1-4662-A840-CAB24B3145B9}.Release|x64.Build.0 = Release|Any CPU {E79D336A-0BA1-4662-A840-CAB24B3145B9}.Release|x86.ActiveCfg = Release|Any CPU {E79D336A-0BA1-4662-A840-CAB24B3145B9}.Release|x86.Build.0 = Release|Any CPU + {E1844E4F-2711-4A04-B70C-D67D5D2C8F31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1844E4F-2711-4A04-B70C-D67D5D2C8F31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1844E4F-2711-4A04-B70C-D67D5D2C8F31}.Debug|x64.ActiveCfg = Debug|Any CPU + {E1844E4F-2711-4A04-B70C-D67D5D2C8F31}.Debug|x64.Build.0 = Debug|Any CPU + {E1844E4F-2711-4A04-B70C-D67D5D2C8F31}.Debug|x86.ActiveCfg = Debug|Any CPU + {E1844E4F-2711-4A04-B70C-D67D5D2C8F31}.Debug|x86.Build.0 = Debug|Any CPU + {E1844E4F-2711-4A04-B70C-D67D5D2C8F31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1844E4F-2711-4A04-B70C-D67D5D2C8F31}.Release|Any CPU.Build.0 = Release|Any CPU + {E1844E4F-2711-4A04-B70C-D67D5D2C8F31}.Release|x64.ActiveCfg = Release|Any CPU + {E1844E4F-2711-4A04-B70C-D67D5D2C8F31}.Release|x64.Build.0 = Release|Any CPU + {E1844E4F-2711-4A04-B70C-D67D5D2C8F31}.Release|x86.ActiveCfg = Release|Any CPU + {E1844E4F-2711-4A04-B70C-D67D5D2C8F31}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -104,6 +120,7 @@ Global {1DF9E750-9308-4E12-8A48-CAABE68FF30A} = {42718370-2C75-4708-8C40-DD59D633C96E} {E73BF13E-A17F-4B0B-8504-C2A299B83DEE} = {42718370-2C75-4708-8C40-DD59D633C96E} {E79D336A-0BA1-4662-A840-CAB24B3145B9} = {A7F8279D-A148-48B8-8492-BDD96CE1E676} + {E1844E4F-2711-4A04-B70C-D67D5D2C8F31} = {32E20E48-2F62-447B-83A2-DB2EA7F61AB3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7C5FBB7F-654C-4CAC-964F-6D71AF3D62F8} diff --git a/src/App/NetDaemon.App/Common/Reactive/ObservableExtensionMethods.cs b/src/App/NetDaemon.App/Common/Reactive/ObservableExtensionMethods.cs index 432573fbb..938726c2b 100644 --- a/src/App/NetDaemon.App/Common/Reactive/ObservableExtensionMethods.cs +++ b/src/App/NetDaemon.App/Common/Reactive/ObservableExtensionMethods.cs @@ -18,5 +18,25 @@ public static IObservable<(EntityState Old, EntityState New)> NDSameStateFor(thi { return observable.Throttle(span); } + + /// + /// Wait for state the specified time + /// + /// + /// Timeout waiting for state + public static IObservable<(EntityState Old, EntityState New)> NDWaitForState(this IObservable<(EntityState Old, EntityState New)> observable, TimeSpan timeout) + { + return observable.Timeout(timeout, Observable.Return((new NetDaemon.Common.EntityState() { State = "TimeOut" }, new NetDaemon.Common.EntityState() { State = "TimeOut" }))).Take(1); + } + + /// + /// Wait for state the default time + /// + /// + public static IObservable<(EntityState Old, EntityState New)> NDWaitForState(this IObservable<(EntityState Old, EntityState New)> observable) + { + return observable.Timeout(TimeSpan.FromSeconds(5), Observable.Return((new NetDaemon.Common.EntityState() { State = "TimeOut" }, new NetDaemon.Common.EntityState() { State = "TimeOut" }))).Take(1); + } + } } \ No newline at end of file diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs b/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs index fa5927ab2..6f2241b53 100644 --- a/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs +++ b/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs @@ -18,6 +18,7 @@ using NetDaemon.Mapping; [assembly: InternalsVisibleTo("NetDaemon.Daemon.Tests")] +[assembly: InternalsVisibleTo("NetDaemon.Test")] namespace NetDaemon.Daemon { diff --git a/tests/NetDaemon.Test/DaemonHostTestBase.cs b/tests/NetDaemon.Test/DaemonHostTestBase.cs new file mode 100644 index 000000000..0ac5a66d5 --- /dev/null +++ b/tests/NetDaemon.Test/DaemonHostTestBase.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Dynamic; +using System.Threading; +using System.Threading.Tasks; +using JoySoftware.HomeAssistant.Client; +using Moq; +using NetDaemon.Common; +using NetDaemon.Common.Reactive; +using NetDaemon.Daemon.Storage; +using Xunit; + +namespace NetDaemon.Daemon.Test +{ + + public partial class DaemonHostTestBase : IAsyncLifetime + { + private readonly NetDaemonHost _defaultDaemonHost; + private readonly Mock _defaultDataRepositoryMock; + private readonly HassClientMock _defaultHassClientMock; + private readonly HttpHandlerMock _defaultHttpHandlerMock; + private readonly LoggerMock _loggerMock; + + internal DaemonHostTestBase() + { + _loggerMock = new LoggerMock(); + _defaultHassClientMock = HassClientMock.DefaultMock; + _defaultDataRepositoryMock = new Mock(); + + _defaultHttpHandlerMock = new HttpHandlerMock(); + _defaultDaemonHost = new NetDaemonHost( + _defaultHassClientMock.Object, + _defaultDataRepositoryMock.Object, + _loggerMock.LoggerFactory, + _defaultHttpHandlerMock.Object); + + } + + public NetDaemonHost DefaultDaemonHost => _defaultDaemonHost; + public Mock DefaultDataRepositoryMock => _defaultDataRepositoryMock; + public HassClientMock DefaultHassClientMock => _defaultHassClientMock; + public HttpHandlerMock DefaultHttpHandlerMock => _defaultHttpHandlerMock; + public LoggerMock LoggerMock => _loggerMock; + + Task IAsyncLifetime.DisposeAsync() + { + return Task.CompletedTask; + } + + /// + /// Gets a object as dynamic + /// + /// The object to turn into dynamic + public dynamic GetDynamicDataObject(string testData = "testdata") + { + var expandoObject = new ExpandoObject(); + dynamic dynamicData = expandoObject; + dynamicData.Test = testData; + return dynamicData; + } + + /// + /// Converts parameters to dynamics + /// + public (dynamic, ExpandoObject) GetDynamicObject(params (string, object)[] dynamicParameters) + { + var expandoObject = new ExpandoObject(); + var dict = expandoObject as IDictionary; + + foreach (var (name, value) in dynamicParameters) + { + dict[name] = value; + } + return (expandoObject, expandoObject); + } + + /// + /// Override for test init function + /// + public virtual Task InitializeAsync() + { + return Task.CompletedTask; + } + + /// + /// Adds an new instance of app + /// + /// The instance of the app to add + public void AddAppInstance(INetDaemonAppBase app) + { + if (string.IsNullOrEmpty(app.Id)) + throw new ArgumentException("Application needs an unique id, please provide it!"); + DefaultDaemonHost.InternalAllAppInstances[app.Id] = app; + DefaultDaemonHost.InternalRunningAppInstances[app.Id] = app; + } + + /// + /// Adds an simple state change event to NetDaemon to trigger apps + /// + /// Unique id of the entity + /// From state + /// To state + public void AddChangedEvent(string entityId, object fromState, object toState) + { + DefaultHassClientMock.AddChangedEvent(entityId, fromState, toState); + } + + /// + /// Adds a full home assistant fake event + /// + /// Event to fake + public void AddChangedEvent(HassEvent hassEvent) + { + DefaultHassClientMock.FakeEvents.Enqueue(hassEvent); + } + + /// + /// Add a fake event + /// + /// The id of the event + /// any custom data provided + public void AddCustomEvent(string eventType, dynamic? data) + { + DefaultHassClientMock.FakeEvents.Enqueue(new HassEvent + { + EventType = eventType, + Data = data + }); + } + + /// + /// Add a face service call event + /// + /// Domain of event + /// Service to call + /// Custom data + public void AddCallServiceEvent(string domain, string service, dynamic data) + { + DefaultHassClientMock.AddCallServiceEvent(domain, service, data); + } + + /// + /// Verify that a service has been called + /// + /// Domain of service + /// The service name + /// Attributes + public void VerifyCallService(string domain, string service, + params (string attribute, object value)[] attributesTuples) + { + DefaultHassClientMock.VerifyCallService(domain, service, attributesTuples); + } + + /// + /// Verify that a service been called specific number of times + /// + /// Service name + /// Times called + public void VerifyCallServiceTimes(string service, Times times) + { + DefaultHassClientMock.VerifyCallServiceTimes(service, times); + } + + public async Task<(Task, CancellationTokenSource)> ReturnRunningDefauldDaemonHostTask(short milliSeconds = 100, bool overrideDebugNotCancel = false) + { + await InitApps(); + var cancelSource = Debugger.IsAttached && !overrideDebugNotCancel + ? new CancellationTokenSource() + : new CancellationTokenSource(milliSeconds); + return (_defaultDaemonHost.Run("host", 8123, false, "token", cancelSource.Token), cancelSource); + } + + public async Task RunDefauldDaemonUntilCanceled(short milliSeconds = 100, bool overrideDebugNotCancel = false) + { + var cancelSource = Debugger.IsAttached && !overrideDebugNotCancel + ? new CancellationTokenSource() + : new CancellationTokenSource(milliSeconds); + try + { + await InitApps(); + await _defaultDaemonHost.Run("host", 8123, false, "token", cancelSource.Token).ConfigureAwait(false); + } + catch (TaskCanceledException) + { + // Expected behaviour + } + } + + public async Task WaitUntilCanceled(Task task) + { + try + { + await task.ConfigureAwait(false); + } + catch (TaskCanceledException) + { + // Expected behaviour + } + } + + public async Task InitApps() + { + foreach (var inst in DefaultDaemonHost.InternalAllAppInstances) + { + await inst.Value.StartUpAsync(_defaultDaemonHost); + } + + foreach (var inst in DefaultDaemonHost.InternalRunningAppInstances) + { + await inst.Value.InitializeAsync(); + inst.Value.Initialize(); + } + } + + protected async Task GetConnectedNetDaemonTask(short milliSeconds = 100, bool overrideDebugNotCancel = false) + { + var cancelSource = Debugger.IsAttached && !overrideDebugNotCancel + ? new CancellationTokenSource() + : new CancellationTokenSource(milliSeconds); + + await InitApps(); + + var daemonTask = _defaultDaemonHost.Run("host", 8123, false, "token", cancelSource.Token); + await WaitForDefaultDaemonToConnect(DefaultDaemonHost, cancelSource.Token); + return daemonTask; + } + + protected async Task WaitForDefaultDaemonToConnect(NetDaemonHost daemonHost, CancellationToken stoppingToken) + { + var nrOfTimesCheckForConnectedState = 0; + + while (!daemonHost.Connected && !stoppingToken.IsCancellationRequested) + { + await Task.Delay(50, stoppingToken).ConfigureAwait(false); + if (nrOfTimesCheckForConnectedState++ > 100) + break; + } + } + } +} \ No newline at end of file diff --git a/tests/NetDaemon.Test/HassClientMock.cs b/tests/NetDaemon.Test/HassClientMock.cs new file mode 100644 index 000000000..7cc10ffa2 --- /dev/null +++ b/tests/NetDaemon.Test/HassClientMock.cs @@ -0,0 +1,245 @@ +using JoySoftware.HomeAssistant.Client; +using Moq; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using NetDaemon.Common; +using NetDaemon.Common.Fluent; +using Xunit; + +namespace NetDaemon.Daemon.Test +{ + public class HassClientMock : Mock + { + internal HassAreas Areas = new HassAreas(); + internal HassDevices Devices = new HassDevices(); + internal HassEntities Entities = new HassEntities(); + internal ConcurrentQueue FakeEvents = new ConcurrentQueue(); + internal ConcurrentDictionary FakeStates = new ConcurrentDictionary(); + + /// + /// Default constructor + /// + public HassClientMock() + { +#pragma warning disable 8619, 8620 + // Setup common mocks + Setup(x => x.ConnectAsync(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + SetupGet(x => x.States).Returns(FakeStates); + + Setup(x => x.GetAllStates(It.IsAny())) + .ReturnsAsync(() => { return (IEnumerable)FakeStates.Values; }); + + Setup(x => x.ReadEventAsync()) + .ReturnsAsync(() => { return FakeEvents.TryDequeue(out var ev) ? ev : null; }); + + Setup(x => x.ReadEventAsync(It.IsAny())) + .ReturnsAsync(() => { return FakeEvents.TryDequeue(out var ev) ? ev : null; }); + + Setup(x => x.GetConfig()).ReturnsAsync(new HassConfig { State = "RUNNING" }); + + Setup(x => x.SetState(It.IsAny(), It.IsAny(), It.IsAny())).Returns( + (entityId, state, attributes) => + { + var fluentAttr = (FluentExpandoObject)attributes; + var attrib = new Dictionary(); + foreach (var attr in (IDictionary)fluentAttr) + attrib[attr.Key] = attr.Value; + + return Task.FromResult(new HassState + { + EntityId = entityId, + State = state, + Attributes = attrib + }); + } + ); + + Setup(n => n.GetAreas()).ReturnsAsync(Areas); + Setup(n => n.GetDevices()).ReturnsAsync(Devices); + Setup(n => n.GetEntities()).ReturnsAsync(Entities); + + // Setup one with area + Devices.Add(new HassDevice { Id = "device_idd", AreaId = "area_idd" }); + Areas.Add(new HassArea { Name = "Area", Id = "area_idd" }); + Entities.Add(new HassEntity + { + EntityId = "light.ligth_in_area", + DeviceId = "device_idd" + }); + } + + public static HassClientMock DefaultMock => new HassClientMock(); + + /// + /// Returns a mock that will always return false when connect to Home Assistant + /// + public static HassClientMock MockConnectFalse + { + get + { + var mock = DefaultMock; + mock.Setup(x => x.ConnectAsync(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + return mock; + } + } + + public void AddCallServiceEvent(string domain, string service, dynamic data) + { + // Todo: Refactor to smth smarter + FakeEvents.Enqueue(new HassEvent + { + EventType = "call_service", + Data = new HassServiceEventData + { + Domain = domain, + Service = service, + Data = data + } + }); + } + + public void AddChangedEvent(string entityId, object fromState, object toState, DateTime lastUpdated, DateTime lastChanged) + { + // Todo: Refactor to smth smarter + FakeEvents.Enqueue(new HassEvent + { + EventType = "state_changed", + Data = new HassStateChangedEventData + { + EntityId = entityId, + NewState = new HassState + { + State = toState, + Attributes = new Dictionary + { + ["device_class"] = "motion" + }, + LastChanged = lastChanged, + LastUpdated = lastUpdated + }, + OldState = new HassState + { + State = fromState, + Attributes = new Dictionary + { + ["device_class"] = "motion" + } + } + } + }); + } + + public void AddChangedEvent(string entityId, object fromState, object toState) + { + FakeEvents.Enqueue(new HassEvent + { + EventType = "state_changed", + Data = new HassStateChangedEventData + { + EntityId = entityId, + NewState = new HassState + { + EntityId = entityId, + State = toState, + Attributes = new Dictionary + { + ["device_class"] = "motion" + }, + LastUpdated = DateTime.Now, + LastChanged = DateTime.Now + }, + OldState = new HassState + { + EntityId = entityId, + State = fromState, + Attributes = new Dictionary + { + ["device_class"] = "motion" + } + } + }, + }); + } + + public void AddCustomEvent(string eventType, dynamic? data) + { + FakeEvents.Enqueue(new HassEvent + { + EventType = eventType, + Data = data + }); + } + + public void AssertEqual(HassState hassState, EntityState entity) + { + Assert.Equal(hassState.EntityId, entity.EntityId); + Assert.Equal(hassState.State, entity.State); + Assert.Equal(hassState.LastChanged, entity.LastChanged); + Assert.Equal(hassState.LastUpdated, entity.LastUpdated); + + if (hassState.Attributes?.Keys == null || entity.Attribute == null) + return; + + foreach (var attribute in hassState.Attributes!.Keys) + { + var attr = entity.Attribute as IDictionary ?? + throw new NullReferenceException($"{nameof(entity.Attribute)} catn be null"); + + Assert.True(attr.ContainsKey(attribute)); + Assert.Equal(hassState.Attributes[attribute], + attr![attribute]); + } + } + + /// + /// Gets a cancellation source that does not timeout if debugger is attached + /// + /// + /// + public CancellationTokenSource GetSourceWithTimeout(int milliSeconds = 100) + { + return (Debugger.IsAttached) + ? new CancellationTokenSource() + : new CancellationTokenSource(milliSeconds); + } + + public void VerifyCallService(string domain, string service, + params (string attribute, object value)[] attributesTuples) + { + var attributes = new FluentExpandoObject(); + foreach (var attributesTuple in attributesTuples) + ((IDictionary)attributes)[attributesTuple.attribute] = attributesTuple.value; + + Verify(n => n.CallService(domain, service, attributes, It.IsAny()), Times.AtLeastOnce); + } + + public void VerifyCallServiceTimes(string service, Times times) + { + Verify(n => n.CallService(It.IsAny(), service, It.IsAny(), It.IsAny()), times); + } + + public void VerifySetState(string entity, string state, + params (string attribute, object value)[] attributesTuples) + { + var attributes = new FluentExpandoObject(); + foreach (var attributesTuple in attributesTuples) + ((IDictionary)attributes)[attributesTuple.attribute] = attributesTuple.value; + + Verify(n => n.SetState(entity, state, attributes), Times.AtLeastOnce); + } + + public void VerifySetStateTimes(string entity, Times times) + { + Verify(n => n.SetState(entity, It.IsAny(), It.IsAny()), times); + } + } +} \ No newline at end of file diff --git a/tests/NetDaemon.Test/HttpHandlerMock.cs b/tests/NetDaemon.Test/HttpHandlerMock.cs new file mode 100644 index 000000000..3cca9fb3f --- /dev/null +++ b/tests/NetDaemon.Test/HttpHandlerMock.cs @@ -0,0 +1,74 @@ +using Moq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using NetDaemon.Common; + +namespace NetDaemon.Daemon.Test +{ + public class HttpClientFactoryMock : Mock + { + private HttpClient? _httpClient; + private MockHttpMessageHandler? _handler; + public MockHttpMessageHandler? MessageHandler => _handler; + + public HttpClientFactoryMock() + { + } + + public void SetResponse(string response, HttpStatusCode statusCode = HttpStatusCode.OK) + { + _handler = new MockHttpMessageHandler(response, statusCode); + _httpClient = new HttpClient(_handler); + Setup(x => x.CreateClient(It.IsAny())).Returns(_httpClient!); + } + } + + public class HttpHandlerMock : Mock + { + private HttpClient? _httpClient; + private MockHttpMessageHandler? _handler; + + public MockHttpMessageHandler? MessageHandler => _handler; + + public HttpHandlerMock() + { + } + + public void SetResponse(string response, HttpStatusCode statusCode = HttpStatusCode.OK) + { + _handler = new MockHttpMessageHandler(response, statusCode); + _httpClient = new HttpClient(_handler); + Setup(x => x.CreateHttpClient(It.IsAny())).Returns(_httpClient!); + } + } + + public class MockHttpMessageHandler : HttpMessageHandler + { + private readonly string _response; + private readonly HttpStatusCode _StatusCode; + + private string? _requestContent; + + public string? RequestContent => _requestContent; + + public MockHttpMessageHandler(string response, HttpStatusCode statusCode = HttpStatusCode.OK) + { + _response = response; + _StatusCode = statusCode; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var responseMessage = new HttpResponseMessage(_StatusCode); + + if (request is object && request.Content is object) + _requestContent = await request.Content.ReadAsStringAsync().ConfigureAwait(false); + + responseMessage.Content = new ByteArrayContent(Encoding.ASCII.GetBytes(_response)); + return responseMessage; + } + } +} \ No newline at end of file diff --git a/tests/NetDaemon.Test/LoggerMock.cs b/tests/NetDaemon.Test/LoggerMock.cs new file mode 100644 index 000000000..f9aea58d5 --- /dev/null +++ b/tests/NetDaemon.Test/LoggerMock.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.Logging; +using Moq; +using System; + +namespace NetDaemon.Daemon.Test +{ + public class LoggerMock + { + public LoggerMock() + { + // Setup the mock + MockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(MockLogger.Object); + } + + public ILoggerFactory LoggerFactory => MockLoggerFactory.Object; + public Mock MockLoggerFactory { get; } = new Mock(); + + public ILogger Logger => MockLogger.Object; + public Mock MockLogger { get; } = new Mock(); + + /// + /// Assert if the log has been used at times + /// + /// The loglevel being checked + /// The Times it has been logged + public void AssertLogged(LogLevel level, Times times) + { + MockLogger.Verify( + x => x.Log( + level, + It.IsAny(), + It.Is((v, t) => true), + It.IsAny(), + It.Is>((v, t) => true)), times); + } + + /// + /// Assert if the log has been used at times + /// + /// The loglevel being checked + /// The Times it has been logged + public void AssertLogged(LogLevel level, string message, Times times) + { + MockLogger.Verify( + x => x.Log( + level, + It.IsAny(), + It.Is((v, t) => v.ToString() == message), + It.IsAny(), + It.Is>((v, t) => true)), times); + } + + /// + /// Assert if the log has been used at times + /// + /// The loglevel being checked + /// The Times it has been logged + public void AssertLogged(LogLevel level, Exception exception, string message, Times times) + { + MockLogger.Verify( + x => x.Log( + level, + It.IsAny(), + It.Is((v, t) => v.ToString() == message), + exception, + It.Is>((v, t) => true)), times); + } + } +} \ No newline at end of file diff --git a/tests/NetDaemon.Test/NetDaemon.Test.csproj b/tests/NetDaemon.Test/NetDaemon.Test.csproj new file mode 100644 index 000000000..bb898efa7 --- /dev/null +++ b/tests/NetDaemon.Test/NetDaemon.Test.csproj @@ -0,0 +1,38 @@ + + + + netcoreapp3.1 + 8.0 + enable + NetDaemon + JoySoftware.NetDaemon.Test + 0.1.29-alpha + helto4real + JoySoftware + A .net core appdaemon for Home Assistant, test components + https://github.com/net-daemon/netdaemon + MIT + + A client for manage the free open source home automations software Home Assisstant written in .net core 3. + Please see https://github.com/net-daemon/netdaemon/blob/dev/README.md for docs. + + First alpha version, expect things to change! + Home Assistant + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/tests/NetDaemon.Test/Tests/RxApp.cs b/tests/NetDaemon.Test/Tests/RxApp.cs new file mode 100644 index 000000000..f47d8886c --- /dev/null +++ b/tests/NetDaemon.Test/Tests/RxApp.cs @@ -0,0 +1,22 @@ + +using System; +using NetDaemon.Common.Reactive; +using System.Reactive.Linq; +namespace NetDaemon.Daemon.Test.Tests +{ + + public class RxApp : NetDaemonRxApp + { + public override void Initialize() + { + Entity("binary_sensor.pir") + .StateChanges + .Where(e => e.New?.State == "on") + .Subscribe(s => + { + Entity("light.thelight").TurnOn(); + } + ); + } + } +} \ No newline at end of file diff --git a/tests/NetDaemon.Test/Tests/TestTheTestClass.cs b/tests/NetDaemon.Test/Tests/TestTheTestClass.cs new file mode 100644 index 000000000..40073e2bd --- /dev/null +++ b/tests/NetDaemon.Test/Tests/TestTheTestClass.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading.Tasks; +using Xunit; +namespace NetDaemon.Daemon.Test.Tests +{ + public class TestTheTestClass : DaemonHostTestBase + { + [Fact] + public async Task TestMyApp() + { + var app = new RxApp { Id = "fakeId" }; + AddAppInstance(app); + var daemon = await GetConnectedNetDaemonTask(); + AddChangedEvent("binary_sensor.pir", "off", "on"); + + + await daemon.ConfigureAwait(false); + VerifyCallService("light", "turn_on", ("entity_id", "light.thelight")); + } + } +} From 8cba0586f211ad9c159e296a0df59c933c2b9ada Mon Sep 17 00:00:00 2001 From: helto4real Date: Mon, 7 Sep 2020 19:20:08 +0200 Subject: [PATCH 2/5] Fix timing when starting admin --- Docker/rootfs/etc/services.d/NetDaemonAdmin/run | 6 +++--- Dockerfile | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Docker/rootfs/etc/services.d/NetDaemonAdmin/run b/Docker/rootfs/etc/services.d/NetDaemonAdmin/run index a79788bd1..cc51b21e2 100755 --- a/Docker/rootfs/etc/services.d/NetDaemonAdmin/run +++ b/Docker/rootfs/etc/services.d/NetDaemonAdmin/run @@ -1,10 +1,10 @@ #!/usr/bin/with-contenv bash -if [[ $NETDAEMON__ADMIN == "true" ]] -then +if [[ $NETDAEMON__ADMIN == "true" ]]; then echo "Starting NetDaemon Admin" cd /admin || exit 1 + exec sleep 10000 exec node ./host/admin.js else exec sleep 2147483647 -fi \ No newline at end of file +fi diff --git a/Dockerfile b/Dockerfile index 43fc479d5..9f9d58a45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,10 +22,10 @@ RUN wget -qO /s6 \ \ && git clone https://github.com/net-daemon/admin.git /admin \ && cd /admin \ - && git checkout tags/1.3.2 \ + && git checkout tags/1.3.3 \ && make deploy \ \ - && rm -fr /var/lib/apt/lists/* \ + && rm -fr /var/lib/apt/lists/* \ && rm -fr /tmp/* /var/{cache,log}/* # Set default values of NetDaemon env From a007861ddafb66ecadb100733491cc4c7b55886d Mon Sep 17 00:00:00 2001 From: helto4real Date: Mon, 7 Sep 2020 19:35:06 +0200 Subject: [PATCH 3/5] Fix test and admin gui startup time --- Docker/rootfs/etc/services.d/NetDaemonAdmin/run | 2 +- tests/NetDaemon.Test/Tests/TestTheTestClass.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Docker/rootfs/etc/services.d/NetDaemonAdmin/run b/Docker/rootfs/etc/services.d/NetDaemonAdmin/run index cc51b21e2..e510d060d 100755 --- a/Docker/rootfs/etc/services.d/NetDaemonAdmin/run +++ b/Docker/rootfs/etc/services.d/NetDaemonAdmin/run @@ -3,7 +3,7 @@ if [[ $NETDAEMON__ADMIN == "true" ]]; then echo "Starting NetDaemon Admin" cd /admin || exit 1 - exec sleep 10000 + exec sleep 15 exec node ./host/admin.js else exec sleep 2147483647 diff --git a/tests/NetDaemon.Test/Tests/TestTheTestClass.cs b/tests/NetDaemon.Test/Tests/TestTheTestClass.cs index 40073e2bd..75fcb07a8 100644 --- a/tests/NetDaemon.Test/Tests/TestTheTestClass.cs +++ b/tests/NetDaemon.Test/Tests/TestTheTestClass.cs @@ -10,7 +10,7 @@ public async Task TestMyApp() { var app = new RxApp { Id = "fakeId" }; AddAppInstance(app); - var daemon = await GetConnectedNetDaemonTask(); + var daemon = await GetConnectedNetDaemonTask(200); AddChangedEvent("binary_sensor.pir", "off", "on"); From 3fe75066f54efae9e6685f64e0370aa5490eb79b Mon Sep 17 00:00:00 2001 From: helto4real Date: Mon, 7 Sep 2020 20:02:19 +0200 Subject: [PATCH 4/5] Remove sleep at start --- Docker/rootfs/etc/services.d/NetDaemonAdmin/run | 1 - 1 file changed, 1 deletion(-) diff --git a/Docker/rootfs/etc/services.d/NetDaemonAdmin/run b/Docker/rootfs/etc/services.d/NetDaemonAdmin/run index e510d060d..08de89395 100755 --- a/Docker/rootfs/etc/services.d/NetDaemonAdmin/run +++ b/Docker/rootfs/etc/services.d/NetDaemonAdmin/run @@ -3,7 +3,6 @@ if [[ $NETDAEMON__ADMIN == "true" ]]; then echo "Starting NetDaemon Admin" cd /admin || exit 1 - exec sleep 15 exec node ./host/admin.js else exec sleep 2147483647 From 2699546d0113971cbc124f060f678d7bf296b32b Mon Sep 17 00:00:00 2001 From: helto4real Date: Mon, 7 Sep 2020 20:19:10 +0200 Subject: [PATCH 5/5] Admin GUI - longer connection timeout --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9f9d58a45..8b32f4bdf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ RUN wget -qO /s6 \ \ && git clone https://github.com/net-daemon/admin.git /admin \ && cd /admin \ - && git checkout tags/1.3.3 \ + && git checkout tags/1.3.4 \ && make deploy \ \ && rm -fr /var/lib/apt/lists/* \