Skip to content

Commit

Permalink
task/unit tests (#1)
Browse files Browse the repository at this point in the history
* Fix typos
* Add some unit tests
  • Loading branch information
melgish authored May 9, 2024
1 parent f16d1a1 commit 401fb31
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 21 deletions.
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"cSpell.words": [
"Anar",
"appsettings",
"Arien",
"Enphase"
]
}
2 changes: 2 additions & 0 deletions Anar.Tests/Anar.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.Testing" Version="8.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="moq" Version="4.20.70" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
Expand Down
104 changes: 104 additions & 0 deletions Anar.Tests/Services/Gateway/GatewayClient.Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using Anar.Services;

using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;

using Moq;
using Moq.Protected;

using System.Net;

namespace Anar.Tests.Services;

public class GatewayClientTests
{
private readonly FakeLogger<GatewayClient> _fakeLogger;
private readonly HttpClient _httpClient;
private readonly Mock<IOptions<GatewayOptions>> _mockOptions;
private readonly Mock<HttpMessageHandler> _mockHandler;
private readonly GatewayOptions _options;

public GatewayClientTests()
{
_options = new GatewayOptions
{
Uri = new Uri("http://localhost/"),
Token = "token",
Interval = TimeSpan.FromSeconds(10),
Layout = [
new Location { SerialNumber = "12345", Azimuth = 0, ArrayName = "Array 1" },
]
};

_fakeLogger = new FakeLogger<GatewayClient>();
_mockOptions = new Mock<IOptions<GatewayOptions>>();
_mockHandler = new Mock<HttpMessageHandler>();
_httpClient = new HttpClient(_mockHandler.Object) {
BaseAddress = _options.Uri,
};

_mockOptions.Setup(o => o.Value).Returns(_options);
}

private GatewayClient CreateGatewayClient()
{
return new GatewayClient(_fakeLogger, _mockOptions.Object, _httpClient);
}

[Fact]
public void Interval_ShouldReturnOptionsValue()
{
var client = CreateGatewayClient();

Assert.Equal(_options.Interval, client.Interval);
}

[Fact]
public async Task GetInvertersAsync_Success_ReturnsInverters()
{
_mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("[{\"SerialNumber\": \"12345\"}]")
})
.Verifiable();

var client = CreateGatewayClient();
var result = await client.GetInvertersAsync();

Assert.Single(result);
Assert.Equal("12345", result.First().SerialNumber);
Assert.NotNull(result.First().Location);
}

[Fact]
public async Task GetInvertersAsync_Success_ReturnsEmpty()
{
_mockHandler
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.NotFound
})
.Verifiable();

var client = CreateGatewayClient();
var result = await client.GetInvertersAsync();

Assert.Empty(result);
Assert.Equal(LogEvent.GetInvertersFailed, _fakeLogger.LatestRecord.Id);
}

}
21 changes: 21 additions & 0 deletions Anar.Tests/Services/Gateway/Location.Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Anar.Services;

namespace Anar.Tests.Services;

public class LocationTest
{
[Fact]
public void Facing_Should_Return_A_Direction()
{
Location north = new(){ Azimuth = 0 };
Assert.Equal("north", north.Facing);
Location east = new() { Azimuth = 90 };
Assert.Equal("east", east.Facing);
Location south = new() { Azimuth = 180 };
Assert.Equal("south", south.Facing);
Location west = new() { Azimuth = 270 };
Assert.Equal("west", west.Facing);
Location unknown = new() { Azimuth = 123 };
Assert.Equal("unknown", unknown.Facing);
}
}
42 changes: 42 additions & 0 deletions Anar.Tests/Services/Influx/DomainExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Anar.Services;

namespace Anar.Tests.Services;

public sealed class DomainExtensionsTests
{
[Fact]
public void ToPointData_WhenLocationIsNotNull_ShouldReturnPointData()
{
var inverter = new Inverter
{
SerialNumber = "123",
LastReportDate = 123456789,
LastReportWatts = 123,
Location = new()
{
ArrayName = "array",
Azimuth = 90,
SerialNumber = "123",
}
};

var point = inverter.ToPointData();

Assert.Equal("inverter,arrayName=array,facing=east,serialNumber=123 watts=123i 123456789", point.ToLineProtocol());
}

[Fact]
public void ToPointData_WhenLocationNull_ShouldReturnPointData()
{
var inverter = new Inverter
{
SerialNumber = "123",
LastReportDate = 123456789,
LastReportWatts = 123,
};

var point = inverter.ToPointData();

Assert.Equal("inverter,serialNumber=123 watts=123i 123456789", point.ToLineProtocol());
}
}
7 changes: 7 additions & 0 deletions Anar/Anar.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,11 @@
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0" />
</ItemGroup>


<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>$(MSBuildProjectName).Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>
1 change: 0 additions & 1 deletion Anar/Services/Gateway/GatewayClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ public async Task<IEnumerable<Inverter>> GetInvertersAsync(CancellationToken can

return inverters;
} catch (Exception ex) {

_logger.LogWarning(LogEvent.GetInvertersFailed, ex, "Failed to get inverters");
return [];
}
Expand Down
2 changes: 1 addition & 1 deletion Anar/Services/Gateway/GatewayOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Anar.Services;

internal sealed class GatewayOptions
public sealed class GatewayOptions
{
/// <summary>
/// Polling interval
Expand Down
4 changes: 2 additions & 2 deletions Anar/Services/Gateway/Location.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ namespace Anar.Services;
/// <summary>
/// Encapsulates the location of a single panel / inverter.
/// </summary>
internal sealed class Location
public sealed class Location
{
/// <summary>
/// Name of the array where the inverter is located.
Expand All @@ -21,7 +21,7 @@ internal sealed class Location
public string SerialNumber { get; init; } = default!;

/// <summary>
/// Convert facing to a string
/// Convert facing to a string.
/// </summary>
public string Facing
{
Expand Down
4 changes: 2 additions & 2 deletions Anar/Services/Influx/DomainExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ namespace Anar.Services;

internal static class DomainExtensions
{
public static PointData ToPointData(this Inverter inverter, WritePrecision precision)
public static PointData ToPointData(this Inverter inverter)
{
var point = PointData
.Measurement("inverter")
.Timestamp(inverter.LastReportDate, precision)
.Timestamp(inverter.LastReportDate, WritePrecision.S)
.Field("watts", inverter.LastReportWatts)
.Tag("serialNumber", inverter.SerialNumber);

Expand Down
4 changes: 1 addition & 3 deletions Anar/Services/Influx/InfluxService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ public async Task WriteAsync(IEnumerable<Inverter> inverters, CancellationToken

// Original timestamp on the inverter to eliminate duplicates.
// Total will be logged using most recent report.
var points = inverters
.Select(i => i.ToPointData(WritePrecision.S))
.ToArray();
var points = inverters.Select(i => i.ToPointData()).ToArray();

await api.WritePointsAsync(
points,
Expand Down
30 changes: 18 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,39 @@ the heavens by the Maia Arien after the fall of the Two Trees.
Anar here monitors my own solar system [heh] gathering data on my inverters
and uploading the data to my InfluxDB database.

Much of this progject was geared towards creating a .NET project the could
Much of this project was geared towards creating a .NET project the could
be configured and run in a docker swarm environment.

# Configuration
Like most .NET apps, you can specify all configuration settings using a combination of config files `appsettings.json`, environment variables, or
Like most .NET apps, you can specify all configuration settings using a
combination of config files `appsettings.json`, environment variables, or
command line options.

In addition, this project supports using Docker configs and secrets as follows.
An envrionment varialbe APP_SETTINGS_DOCKER can be set to identify an
additional location for appsettings.config. This can be `/configname` to load
from a docker config, or `/run/secrets/secretname` to load from a docker secret.
An environment variable APP_SETTINGS_DOCKER can be set to identify an
additional location for an appsettings style config file. This can be
`/config-name` to load from a docker config, or `/run/secrets/secret-name` to
load from a docker secret.

## GatewayOptions
This section controls settings for communcating with an Enphase IQ gateway.
This section controls settings for communicating with an Enphase IQ gateway.
Enphase does not make direct access easy.

### GatewayOptions : Inteval (TimeSpan) default = 0.00:05:00
### GatewayOptions : Interval (TimeSpan) default = 0.00:05:00
This setting controls how often to poll the data from the gateway.

### GatewayOptions : Layout (Location[]) default []
This optional setting provides a way to embed layout data directly into
appsettings. Layout data can be used to augment the InfluxDB data with
`appsettings.json` Layout data can be used to augment the InfluxDB data with
additional tags, but requires detailed information about your installation.

### GatewayOptions : LayoutFile (string)
This optional setting provides an alternative to Layout above. Instead of embedding the location array directly, layout information can be imported from
This optional setting provides an alternative to Layout above. Instead of
embedding the location array directly, layout information can be imported from
`array_layout_x.json` which is one of the files downloaded in the background
when you view your system on the [Enlighten Website](https://enlighten.enphaseenergy.com/). You'll need to use dev tools to capture this file.
when you view your system on the
[Enlighten Website](https://enlighten.enphaseenergy.com/). You'll need to use
browser developer tools to capture this file.

### GatewayOptions : Thumbprint (string)
This is the SHA-1 value of the self-signed certificate of your Enphase gateway.
Expand All @@ -42,14 +47,15 @@ of hex digits with no punctuation.

### GatewayOptions : Token (string)
This is the security token the system should use when accessing your gateway.
[This Technical Brief](https://enphase.com/download/accessing-iq-gateway-local-apis-or-local-ui-token-based-authentication) describes one way to get one.
[This Technical Brief](https://enphase.com/download/accessing-iq-gateway-local-apis-or-local-ui-token-based-authentication)
describes one way to get one.

### GatewayOptions : Uri (string)
This is the web address of your gateway. Usually it is going to be a local IP
like `https://192.168.1.10`, but your setup may be different than mine.

## InfluxOptions
These opitons control how the app will connect to an InfluxDB database.
These options control how the app will connect to an InfluxDB database.

### InfluxOptions : Bucket (string)
This is the data bucket to insert data to.
Expand Down

0 comments on commit 401fb31

Please sign in to comment.