Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Api/.NET API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
<AssemblyName>KoenZomers.Tado.Api</AssemblyName>
<RootNamespace>KoenZomers.Tado.Api</RootNamespace>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>0.7.0.0</Version>
<Version>0.7.1.0</Version>
<Authors>Koen Zomers</Authors>
<Description>API in .NET9 and .NET10 to communicate with a Tado home heating/cooling system</Description>
<PackageProjectUrl>https://github.com/KoenZomers/TadoApi</PackageProjectUrl>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\KoenZomers.Tado.Api.snk</AssemblyOriginatorKeyFile>
<PackageReleaseNotes>- Added target .NET10 and improved new device code grant flow with cancellation tokens</PackageReleaseNotes>
<PackageReleaseNotes>Added TokenChangedEvent and fixed the refresh flow of the token.</PackageReleaseNotes>
<PackageLicenseUrl></PackageLicenseUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
Expand Down
159 changes: 90 additions & 69 deletions Api/Controllers/Tado.cs

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Api/Exceptions/AuthenticationExpiredException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
/// </summary>
public class AuthenticationExpiredException : Exception
{
private const string defaultMessage = "The authentication has expired. You have to reauthenticate.";
private const string DefaultMessage = "The authentication has expired. You have to reauthenticate.";

public AuthenticationExpiredException() : base(defaultMessage)
public AuthenticationExpiredException(string? message) : base(message ?? DefaultMessage)
{
}

public AuthenticationExpiredException(Exception innerException, string message = defaultMessage) : base(message, innerException)
public AuthenticationExpiredException(Exception innerException, string? message) : base(message ?? DefaultMessage, innerException)
{
}
}
12 changes: 12 additions & 0 deletions Api/Models/Authentication/TokenChangedEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace KoenZomers.Tado.Api.Models.Authentication;

/// <summary>
/// EventArgs to pass the new token when it has been changed/refreshed
/// </summary>
public sealed class TokenChangedEventArgs(Token token) : EventArgs
{
/// <summary>
/// The new token
/// </summary>
public Token Token { get; } = token;
}
22 changes: 21 additions & 1 deletion KoenZomers.Tado.Api.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

120 changes: 42 additions & 78 deletions UnitTest/AuthenticationTest.cs
Original file line number Diff line number Diff line change
@@ -1,89 +1,53 @@
using System.Diagnostics;

namespace KoenZomers.Tado.UnitTest;
namespace KoenZomers.Tado.Api;

/// <summary>
/// Unit Tests to validate authenticating against the Tado API
/// Integration test to validate the authenticating against the Tado API
/// Run all tests in the correct order. Make sure to register the device with the code between while running Test2!!
/// </summary>
[TestClass]
public class AuthenticationTest : BaseTest
public class AuthenticationIntegrationTest : IntegrationTestBase
{
/// <summary>
/// Test being able to retrieve an authorization URL
/// </summary>
/// <returns></returns>
[TestMethod]
public async Task GetDeviceCodeAuthenticationTest()
{
if (Service is null) Assert.Fail("Service not available");

var authenticationRequest = await Service.GetDeviceCodeAuthentication(CancellationToken.None);

Assert.IsNotNull(authenticationRequest, "Failed to instantiate device authentication flow");
}

/// <summary>
/// Test requesting a device authorization code and waiting for it to be completed
/// </summary>
/// <returns></returns>
[TestMethod]
public async Task WaitForDeviceCodeAuthenticationToCompleteTest()
public async Task Run()
{
if (Service is null) Assert.Fail("Service not available");

var authenticationRequest = await Service.GetDeviceCodeAuthentication(CancellationToken.None);

Assert.IsNotNull(authenticationRequest, "Failed to instantiate device authentication flow");

Debug.WriteLine($"URL for authentication: {authenticationRequest.VerificationUriComplete}");

var tokenResponse = await Service.WaitForDeviceCodeAuthenticationToComplete(authenticationRequest, CancellationToken.None);

Assert.IsNotNull(tokenResponse, "Failed to complete device authentication flow");

Debug.WriteLine($"Access token: {tokenResponse.AccessToken}");
Debug.WriteLine($"Refresh token: {tokenResponse.RefreshToken}");
Assert.IsNotNull(Service, "Service not available.");
Assert.IsNotNull(Configuration, "Configuration not available (appsettings.json).");
Assert.IsNotNull(Configuration.TadoHomeId, "TadoHomeId must be set in the appsettings.json for this test to work.");

// Step 1: Retrieve a new device code
var deviceAuthorizationResponse = await Service.GetDeviceCodeAuthentication(TestContext.CancellationToken);
Assert.IsNotNull(deviceAuthorizationResponse, "Failed to instantiate device authentication flow.");

// Step2: Go to the tado URL and authenticate your device
Debug.WriteLine("URL for authentication: {0}", deviceAuthorizationResponse.VerificationUriComplete);
Console.WriteLine("URL for authentication: {0}", deviceAuthorizationResponse.VerificationUriComplete);
TestContext.WriteLine("URL for authentication: {0}", deviceAuthorizationResponse.VerificationUriComplete);

// Step3: Wait for the user to complete the authentication
var token = await Service.WaitForDeviceCodeAuthenticationToComplete(deviceAuthorizationResponse, TestContext.CancellationToken);
Assert.IsNotNull(token, "Failed to complete device authentication flow.");

// Step4: Try to refresh the access token with the refresh token
var newToken = await Service.GetAccessTokenWithRefreshToken(token.RefreshToken!, TestContext.CancellationToken);
Assert.IsNotNull(newToken, "Failed to retrieve new access token with refresh token.");
Assert.AreNotEqual(token.RefreshToken, newToken.RefreshToken);
Assert.AreNotEqual(token.AccessToken, newToken.AccessToken);

// Step5: Try to fetch device data with the access token and let the thermostat say 'Hi'.
Service.Authenticate(newToken);
var devices = await Service.GetDevices(Configuration!.TadoHomeId!.Value);
Assert.IsNotNull(devices, "Failed to retrieve devices.");
Assert.IsNotEmpty(devices, "No devices found.");

var thermostatDevices = devices.Where(x => x.DeviceType!.StartsWith("RU")).ToArray();
var success = thermostatDevices.Length > 0;
foreach (var device in thermostatDevices)
{
success &= await Service.SayHi(device.SerialNo!, TestContext.CancellationToken);
}
Assert.IsTrue(success, "Failed to retrieve information from Tado and say Hi to devices.");
}

/// <summary>
/// Test requesting an access token with a refresh token
/// </summary>
/// <returns></returns>
[TestMethod]
public async Task GetAccessTokenWithRefreshTokenTest()
{
if (Service is null) Assert.Fail("Service not available");

var tokenResponse = await Service.GetAccessTokenWithRefreshToken("xxx", CancellationToken.None);

Assert.IsNotNull(tokenResponse, "Failed to retrieve access token with refresh token");
}

/// <summary>
/// Test requesting the Me endpoint from Tado
/// </summary>
/// <returns></returns>
[TestMethod]
public async Task GetMeTest()
{
if (Service is null) Assert.Fail("Service not available");

var authenticationRequest = await Service.GetDeviceCodeAuthentication(CancellationToken.None);

Assert.IsNotNull(authenticationRequest, "Failed to instantiate device authentication flow");

Debug.WriteLine($"URL for authentication: {authenticationRequest.VerificationUriComplete}");

var tokenResponse = await Service.WaitForDeviceCodeAuthenticationToComplete(authenticationRequest, CancellationToken.None);

Assert.IsNotNull(tokenResponse, "Failed to complete device authentication flow");

Service.Authenticate(tokenResponse);

var devices = await Service.GetDevices(Configuration.TadoHomeId.Value);

var me = await Service.SayHi("xxxx");

Assert.IsNotNull(me, "Failed to retrieve information from Tado");
}
public TestContext TestContext { get; set; }
}
6 changes: 3 additions & 3 deletions UnitTest/BaseTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace KoenZomers.Tado.UnitTest;
namespace KoenZomers.Tado.Api;

/// <summary>
/// Base functionality shared by all Unit Tests
/// </summary>
public abstract class BaseTest
public abstract class IntegrationTestBase
{
/// <summary>
/// The service provider for the application
Expand All @@ -27,7 +27,7 @@ public abstract class BaseTest
/// <summary>
/// Instantiate the Unit Test by creating the service provider and retrieving the service instance to be tested
/// </summary>
public BaseTest()
protected IntegrationTestBase()
{
var services = new ServiceCollection();

Expand Down
4 changes: 2 additions & 2 deletions UnitTest/UnitTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>KoenZomers.TadoApi.UnitTest</AssemblyName>
<RootNamespace>KoenZomers.TadoApi.UnitTest</RootNamespace>
<AssemblyName>KoenZomers.Tado.Api.UnitTest</AssemblyName>
<RootNamespace>KoenZomers.Tado.Api</RootNamespace>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading