From 1f9b2d078cd6fc2cf2d101a00fe3156147fa3b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomas=20Hellstr=C3=B6m?= Date: Sun, 23 Jan 2022 20:09:12 +0100 Subject: [PATCH] Add integration tests. --- .github/workflows/ci_integration_tests.yaml | 85 +++++++++++++++++++ .../Integration/HA/config/configuration.yaml | 15 +++- .../NetDaemon.Tests.Integration/BasicTest.cs | 78 +++++++++++++++++ .../Helpers/TestFixture.cs | 47 ++++++++++ .../NetDaemon.Tests.Integration.csproj | 50 +++++++++++ .../NetDaemon.Tests.Integration/Startup.cs | 28 ++++++ .../_appsettings.Development.json | 20 +++++ .../apps/Config.yaml | 0 .../appsettings.json | 20 +++++ 9 files changed, 339 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ci_integration_tests.yaml create mode 100644 tests/Integration/NetDaemon.Tests.Integration/BasicTest.cs create mode 100644 tests/Integration/NetDaemon.Tests.Integration/Helpers/TestFixture.cs create mode 100644 tests/Integration/NetDaemon.Tests.Integration/NetDaemon.Tests.Integration.csproj create mode 100644 tests/Integration/NetDaemon.Tests.Integration/Startup.cs create mode 100644 tests/Integration/NetDaemon.Tests.Integration/_appsettings.Development.json create mode 100644 tests/Integration/NetDaemon.Tests.Integration/apps/Config.yaml create mode 100644 tests/Integration/NetDaemon.Tests.Integration/appsettings.json diff --git a/.github/workflows/ci_integration_tests.yaml b/.github/workflows/ci_integration_tests.yaml new file mode 100644 index 000000000..be3e0c55e --- /dev/null +++ b/.github/workflows/ci_integration_tests.yaml @@ -0,0 +1,85 @@ +name: Integration tests with Home Assistant +on: + pull_request: + branches: + - main + - dev + push: + branches: + - main + - dev + schedule: + - cron: "0 2 * * *" # run at 2 AM UTC + +jobs: + init: + name: Initialize tests with Home Assistant + runs-on: ubuntu-latest + outputs: + versions: ${{ steps.versions.outputs.versions }} + steps: + - name: Set versions + id: versions + run: | + versions='"stable", "beta"' + if ${{ github.event_name == 'schedule' }}; then + versions+=', "dev"' + fi + echo "[${versions}]" + echo "::set-output name=versions::[${versions}]" + + test_with_home_assistant: + name: Integration tests with Home Assistant + needs: init + strategy: + fail-fast: false + matrix: + channel: ${{ fromJson(needs.init.outputs.versions) }} + with_integration: [yes, no] + runs-on: ubuntu-latest + steps: + - name: 📥 Checkout the repository + uses: actions/checkout@main + + - name: 🥅 Install .Net 6 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: "6.0.x" # SDK Version + + - name: 🛠️ Build code + run: dotnet build + + - name: 🔽 Download HA integration + if: matrix.with_integration == 'yes' + run: | + git clone --depth 1 https://github.com/net-daemon/integration.git /tmp/integration + mv /tmp/integration/custom_components ${{github.workspace}}/tests/Integration/HA/config/ + mv /tmp/integration/.github/test_configuration/.storage ${{github.workspace}}/tests/Integration/HA/config/ + + - name: 👷 Setup Home Assistant + id: homeassistant + uses: ludeeus/setup-homeassistant@main + with: + tag: ${{ matrix.channel }} + config-dir: ./tests/Integration/HA/config + + - name: 🧪 Run integration tests + env: + HomeAssistant__Token: ${{ steps.homeassistant.outputs.token }} + HomeAssistant__Host: "127.0.0.1" + NetDaemon__ApplicationConfigurationFolder: ${{github.workspace}}/tests/Integration/NetDaemon.Tests.Integration/apps + NetDaemon__Admin: false + run: | + dotnet test ${{github.workspace}}/tests/Integration/NetDaemon.Tests.Integration + + - name: Get Home Assistant log in failure + if: ${{ failure() }} + run: docker logs ${{ steps.homeassistant.outputs.container_name }} + + - name: Discord failure notification + if: ${{ github.event_name == 'schedule' && failure() }} + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_ACTION_FAILURE }} + uses: Ilshidur/action-discord@0.3.2 + with: + args: "[Scheduled action failed!](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}})" diff --git a/tests/Integration/HA/config/configuration.yaml b/tests/Integration/HA/config/configuration.yaml index 9f1b2d42e..0fb88e92a 100644 --- a/tests/Integration/HA/config/configuration.yaml +++ b/tests/Integration/HA/config/configuration.yaml @@ -1,6 +1,8 @@ - # Configure a default setup of Home Assistant (frontend, api, etc) default_config: +http: + # ssl_certificate: /config/ssl/homeassistant.local.pem + # ssl_key: /config/ssl/homeassistant.local-key.pem # Text to speech tts: @@ -20,8 +22,13 @@ input_select: options: - Visitors - Visitors with kids - - Home Alone + - Home Alone +input_text: + test_result: + name: Test results + initial: No tests is run yet + template: - trigger: - platform: webhook @@ -36,5 +43,5 @@ automation: !include automations.yaml script: !include scripts.yaml scene: !include scenes.yaml -logger: - default: debug +#logger: +# default: debug diff --git a/tests/Integration/NetDaemon.Tests.Integration/BasicTest.cs b/tests/Integration/NetDaemon.Tests.Integration/BasicTest.cs new file mode 100644 index 000000000..ccc8f2b2d --- /dev/null +++ b/tests/Integration/NetDaemon.Tests.Integration/BasicTest.cs @@ -0,0 +1,78 @@ +using System; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; +using System.Threading.Tasks; +using FluentAssertions; +using NetDaemon.AppModel; +using NetDaemon.HassModel.Common; +using NetDaemon.HassModel.Entities; +using NetDaemon.Tests.Integration.Helpers; +using Xunit; + +namespace NetDaemon.Tests.Integration; + +[NetDaemonApp] +public class BasicApp +{ + public BasicApp( + IHaContext haContext + ) + { + haContext.StateChanges() + .Where(n => + n.Entity.EntityId == "input_select.who_cooks" + ) + .Subscribe( + s => haContext.CallService("input_text", "set_value", + ServiceTarget.FromEntities("input_text.test_result"), new {value = s.New?.State}) + ); + } +} + +public class BasicTests : IClassFixture +{ + private readonly IHaContext _haContext; + + public BasicTests( + MakeSureNetDaemonIsRunningFixture _, + IHaContext haContext + ) + { + _haContext = haContext; + } + + [Fact] + public async Task BasicTestApp_ShouldChangeStateOfInputTextToTheStateOfInputSelect_WhenChange() + { + var optionToSet = GetDifferentOptionThanCurrentlySelected(); + + var waitTask = _haContext.StateChanges() + .Where(n => n.Entity.EntityId == "input_text.test_result") + .Timeout(TimeSpan.FromMilliseconds(5000)) + .FirstAsync() + .ToTask(); + + _haContext.CallService( + "input_select", + "select_option", + ServiceTarget.FromEntities("input_select.who_cooks"), + new {option = optionToSet}); + + var result = await waitTask.ConfigureAwait(false); + + result.New!.State.Should().Be(optionToSet); + } + + private string GetDifferentOptionThanCurrentlySelected() + { + var currentState = _haContext.GetState("input_select.who_cooks")?.State + ?? throw new InvalidOperationException(); + + var useOption = currentState switch + { + "Paulus" => "Anne Therese", + _ => "Paulus" + }; + return useOption; + } +} \ No newline at end of file diff --git a/tests/Integration/NetDaemon.Tests.Integration/Helpers/TestFixture.cs b/tests/Integration/NetDaemon.Tests.Integration/Helpers/TestFixture.cs new file mode 100644 index 000000000..fe09a0d0a --- /dev/null +++ b/tests/Integration/NetDaemon.Tests.Integration/Helpers/TestFixture.cs @@ -0,0 +1,47 @@ +using System.Threading; +using System.Threading.Tasks; +using NetDaemon.Client.Common; +using Xunit; + +namespace NetDaemon.Tests.Integration.Helpers; + +/// +/// This fixture is ran once per test session and will +/// make sure Home Assistant is running and we have a +/// valid connection before tests start +/// +public class MakeSureNetDaemonIsRunningFixture : IAsyncLifetime +{ + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private readonly IHomeAssistantRunner _homeAssistantRunner; + + public MakeSureNetDaemonIsRunningFixture( + IHomeAssistantRunner homeAssistantRunner + ) + { + _homeAssistantRunner = homeAssistantRunner; + } + + public async Task InitializeAsync() + { + var connectionRetries = 0; + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + if (_homeAssistantRunner is not null) + break; + if (connectionRetries++ > 20) // 40 seconds and we give up + return; + await Task.Delay(2000, _cancellationTokenSource.Token).ConfigureAwait(false); + } + + // Introduce a small delay so everything is connected and logged + await Task.Delay(2000, _cancellationTokenSource.Token).ConfigureAwait(false); + } + + public Task DisposeAsync() + { + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/tests/Integration/NetDaemon.Tests.Integration/NetDaemon.Tests.Integration.csproj b/tests/Integration/NetDaemon.Tests.Integration/NetDaemon.Tests.Integration.csproj new file mode 100644 index 000000000..7151c422a --- /dev/null +++ b/tests/Integration/NetDaemon.Tests.Integration/NetDaemon.Tests.Integration.csproj @@ -0,0 +1,50 @@ + + + + net6.0 + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + ..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\6.0.0\Microsoft.Extensions.Hosting.Abstractions.dll + + + + + + + + + + + Always + + + Always + + + + + Always + + + + diff --git a/tests/Integration/NetDaemon.Tests.Integration/Startup.cs b/tests/Integration/NetDaemon.Tests.Integration/Startup.cs new file mode 100644 index 000000000..37706a5f9 --- /dev/null +++ b/tests/Integration/NetDaemon.Tests.Integration/Startup.cs @@ -0,0 +1,28 @@ +using System.Reflection; +using Microsoft.Extensions.Hosting; +using NetDaemon.AppModel; +using NetDaemon.Runtime; + +namespace NetDaemon.Tests.Integration; + +/// +/// Startup class +/// +/// +/// XUnit.DependencyInjection logic finds the startup here and we can make our custom +/// host builder that allows dependency injection in test classes +/// +public class Startup +{ + public IHostBuilder CreateHostBuilder(AssemblyName assemblyName) + { + return Host.CreateDefaultBuilder() + .UseNetDaemonAppSettings() + .UseNetDaemonRuntime() + .ConfigureServices((_, services) => + services + .AddAppsFromAssembly(Assembly.GetExecutingAssembly()) + .AddNetDaemonStateManager() + ); + } +} \ No newline at end of file diff --git a/tests/Integration/NetDaemon.Tests.Integration/_appsettings.Development.json b/tests/Integration/NetDaemon.Tests.Integration/_appsettings.Development.json new file mode 100644 index 000000000..10a6b2a87 --- /dev/null +++ b/tests/Integration/NetDaemon.Tests.Integration/_appsettings.Development.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Warning" + }, + "ConsoleThemeType": "System" + }, + "HomeAssistant": { + "Host": "ENTER YOUR IP TO Development Home Assistant here", + "Port": 8124, + "Ssl": false, + "Token": "ENTER YOUR TOKEN", + "InsecureBypassCertificateErrors": false + }, + "NetDaemon": { + "Admin": false, + "ApplicationConfigurationFolder": "./apps" + } +} \ No newline at end of file diff --git a/tests/Integration/NetDaemon.Tests.Integration/apps/Config.yaml b/tests/Integration/NetDaemon.Tests.Integration/apps/Config.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/tests/Integration/NetDaemon.Tests.Integration/appsettings.json b/tests/Integration/NetDaemon.Tests.Integration/appsettings.json new file mode 100644 index 000000000..f97f74f95 --- /dev/null +++ b/tests/Integration/NetDaemon.Tests.Integration/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Warning" + }, + "ConsoleThemeType": "Ansi" + }, + "HomeAssistant": { + "Host": "localhost", + "Port": 8123, + "Ssl": false, + "Token": "", + "InsecureBypassCertificateErrors": false + }, + "NetDaemon": { + "Admin": true, + "ApplicationConfigurationFolder": "./apps" + } +} \ No newline at end of file