-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* added test project * added code coverage tooling Per directions on https://seankilleen.com/2024/03/beautiful-net-test-reports-using-github-actions/ * only run code coverage reporting on Linux * basic sanity checks for NonZeroUInt32 * added topic validator
- Loading branch information
1 parent
863ab50
commit ad71345
Showing
9 changed files
with
256 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,8 @@ on: | |
|
||
jobs: | ||
test: | ||
# Permissions this GitHub Action needs for other things in GitHub | ||
permissions: write-all | ||
name: Test-${{matrix.os}} | ||
runs-on: ${{matrix.os}} | ||
|
||
|
@@ -39,4 +41,52 @@ jobs: | |
run: dotnet build -c Release | ||
|
||
- name: "dotnet test" | ||
run: dotnet test -c Release | ||
run: dotnet test --configuration Release --verbosity normal --logger trx --collect:"XPlat Code Coverage" | ||
|
||
- name: Combine Coverage Reports | ||
if: runner.os == 'Linux' | ||
uses: danielpalme/[email protected] | ||
with: | ||
reports: "**/*.cobertura.xml" | ||
targetdir: "${{ github.workspace }}" | ||
reporttypes: "Cobertura" | ||
verbosity: "Info" | ||
title: "Code Coverage" | ||
tag: "${{ github.run_number }}_${{ github.run_id }}" | ||
customSettings: "" | ||
toolpath: "reportgeneratortool" | ||
|
||
- name: Publish Code Coverage Report | ||
if: runner.os == 'Linux' | ||
uses: irongut/[email protected] | ||
with: | ||
filename: "Cobertura.xml" | ||
badge: true | ||
fail_below_min: false | ||
format: markdown | ||
hide_branch_rate: false | ||
hide_complexity: false | ||
indicators: true | ||
output: both | ||
thresholds: "10 30" | ||
|
||
- name: Add Coverage PR Comment | ||
if: github.event_name == 'pull_request' && runner.os == 'Linux' | ||
uses: marocchino/sticky-pull-request-comment@v2 | ||
with: | ||
recreate: true | ||
path: code-coverage-results.md | ||
|
||
- name: Upload Test Result Files | ||
if: runner.os == 'Linux' | ||
uses: actions/upload-artifact@v4 | ||
with: | ||
name: test-results | ||
path: ${{ github.workspace }}/**/TestResults/**/* | ||
retention-days: 5 | ||
|
||
- name: Publish Test Results | ||
if: always() && runner.os == 'Linux' | ||
uses: EnricoMi/[email protected] | ||
with: | ||
trx_files: "${{ github.workspace }}/**/*.trx" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
// ----------------------------------------------------------------------- | ||
// <copyright file="MqttTopicValidator.cs" company="Petabridge, LLC"> | ||
// Copyright (C) 2024 - 2024 Petabridge, LLC <https://petabridge.com> | ||
// </copyright> | ||
// ----------------------------------------------------------------------- | ||
|
||
namespace TurboMqtt.Core; | ||
|
||
public static class MqttTopicValidator | ||
{ | ||
/// <summary> | ||
/// Validates an MQTT topic ID for publishing or subscribing. | ||
/// </summary> | ||
/// <param name="topic">The topic to validate.</param> | ||
/// <param name="forSubscription">Specifies whether the validation is for subscribing (true) or publishing (false).</param> | ||
/// <returns>A tuple indicating whether the topic is valid and an error message if it is not.</returns> | ||
public static (bool IsValid, string ErrorMessage) ValidateTopic(string topic, bool forSubscription = true) | ||
{ | ||
// Check if the topic is empty | ||
if (string.IsNullOrEmpty(topic)) | ||
{ | ||
return (false, "Topic must not be empty."); | ||
} | ||
|
||
// Check if the topic contains the null character | ||
if (topic.Contains('\0')) | ||
{ | ||
return (false, "Topic must not contain null characters."); | ||
} | ||
|
||
// Check for invalid wildcard usage if for publishing | ||
if (!forSubscription) | ||
{ | ||
if (topic.Contains('+') || topic.Contains('#')) | ||
{ | ||
return (false, "Wildcards ('+' and '#') are not allowed in topics for publishing."); | ||
} | ||
} | ||
|
||
// Validate wildcards for subscription | ||
if (forSubscription) | ||
{ | ||
// Validate '+' | ||
int indexOfPlus = topic.IndexOf('+'); | ||
while (indexOfPlus != -1) | ||
{ | ||
if ((indexOfPlus > 0 && topic[indexOfPlus - 1] != '/') || | ||
(indexOfPlus < topic.Length - 1 && topic[indexOfPlus + 1] != '/')) | ||
{ | ||
return (false, "Single-level wildcard '+' must be located between slashes or at the beginning/end of the topic."); | ||
} | ||
indexOfPlus = topic.IndexOf('+', indexOfPlus + 1); | ||
} | ||
|
||
// Validate '#' | ||
if (topic.Contains('#') && !topic.EndsWith("/#") && !topic.Equals("#")) | ||
{ | ||
return (false, "Multi-level wildcard '#' must be at the end of the topic or after a '/'."); | ||
} | ||
} | ||
|
||
// Check for forbidden use of '$' at the beginning by clients | ||
if (topic.StartsWith($"$")) | ||
{ | ||
return (false, "Topics starting with '$' are reserved and should not be used by clients for publishing."); | ||
} | ||
|
||
// Optional: Check for excessive length (implementation-specific limit) | ||
if (topic.Length > 65535) // Example maximum length | ||
{ | ||
return (false, "Topic exceeds the maximum length allowed."); | ||
} | ||
|
||
// If all checks pass | ||
return (true, "Topic is valid."); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
using System.Runtime.CompilerServices; | ||
|
||
[assembly: InternalsVisibleTo("TurboMqtt.Core.Tests")] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
global using Xunit; | ||
global using FluentAssertions; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// ----------------------------------------------------------------------- | ||
// <copyright file="MqttTopicValidatorSpecs.cs" company="Petabridge, LLC"> | ||
// Copyright (C) 2024 - 2024 Petabridge, LLC <https://petabridge.com> | ||
// </copyright> | ||
// ----------------------------------------------------------------------- | ||
|
||
namespace TurboMqtt.Core.Tests; | ||
|
||
public class MqttTopicValidatorSpecs | ||
{ | ||
public static readonly TheoryData<string> FailureCases = new TheoryData<string>( | ||
|
||
"foo/bar+", // invalid wildcard position | ||
"home/kit+chen/light", // invalid wildcard position | ||
"home/+/kitchen+", // invalid wildcard position | ||
"home+kitchen/light", // invalid wildcard position | ||
|
||
// generate some invalid uses of the '#' wildcard | ||
"foo/#/bar", // invalid wildcard position | ||
|
||
// generate some invalid uses of the '$' characterq | ||
"$foo/bar", // invalid use of '$' character | ||
|
||
// generate some invalid uses of the null character | ||
"foo\0bar" // invalid use of null character | ||
); | ||
|
||
[Theory] | ||
[MemberData(nameof(FailureCases))] | ||
public void ShouldFailValidationForTopicSubscription(string topic) | ||
{ | ||
var result = MqttTopicValidator.ValidateTopic(topic, true); | ||
result.IsValid.Should().BeFalse(); | ||
} | ||
|
||
public static readonly TheoryData<string> SuccessCases = new TheoryData<string>( | ||
"home/kitchen/light", | ||
"home/kitchen/temperature", | ||
"home/kitchen/humidity", | ||
"home/+/pressure", | ||
"home/kitchen/pressure/#" | ||
); | ||
|
||
[Theory] | ||
[MemberData(nameof(SuccessCases))] | ||
public void ShouldPassValidationForTopicSubscription(string topic) | ||
{ | ||
var result = MqttTopicValidator.ValidateTopic(topic, true); | ||
result.IsValid.Should().BeTrue(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
namespace TurboMqtt.Core.Tests; | ||
|
||
public class NonZeroUintTests | ||
{ | ||
[Fact] | ||
public void ShouldFailOnZero() | ||
{ | ||
Assert.Throws<ArgumentOutOfRangeException>(() => new NonZeroUInt32(0)); | ||
} | ||
|
||
[Fact] | ||
public void ShouldSucceedOnNonZero() | ||
{ | ||
var nonZero = new NonZeroUInt32(1); | ||
Assert.Equal(1u, nonZero.Value); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
|
||
<IsPackable>false</IsPackable> | ||
<IsTestProject>true</IsTestProject> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.NET.Test.Sdk"/> | ||
<PackageReference Include="xunit"/> | ||
<PackageReference Include="xunit.runner.visualstudio"> | ||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
<PrivateAssets>all</PrivateAssets> | ||
</PackageReference> | ||
<PackageReference Include="coverlet.collector"> | ||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
<PrivateAssets>all</PrivateAssets> | ||
</PackageReference> | ||
<PackageReference Include="FluentAssertions"/> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\src\TurboMqtt.Core\TurboMqtt.Core.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |