Skip to content
Merged
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
26 changes: 26 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
</PropertyGroup>

<ItemGroup Label="Default Test Dependencies" Condition="'$(IsTestProject)' == 'true'">
<PackageReference Include="coverlet.msbuild" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.Testing" />
<PackageReference Include="NSubstitute" />
Expand All @@ -88,6 +89,31 @@
</PackageReference>
</ItemGroup>

<!-- Global coverage settings applied to all test projects -->
<PropertyGroup Condition="'$(IsTestProject)' == 'true'" Label="Coverage">
<CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>lcov,opencover,cobertura</CoverletOutputFormat>
<CoverletOutput>$(MSBuildThisFileDirectory)TestResults/coverage/$(MSBuildProjectName).</CoverletOutput>
<ExcludeByAttribute>GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute</ExcludeByAttribute>
<ExcludeByFile>**/*Program.cs;**/*Startup.cs;**/*GlobalUsings.cs</ExcludeByFile>
<UseSourceLink>true</UseSourceLink>
<!-- Enforce 100% line coverage; branch coverage is informative only -->
<Threshold>100</Threshold>
Comment on lines +100 to +101
Copy link

Copilot AI Aug 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] A 100% line coverage threshold may be too strict and could encourage writing tests just to meet coverage rather than testing meaningful behavior. Consider using a more practical threshold like 90-95% to allow for reasonable exceptions.

Suggested change
<!-- Enforce 100% line coverage; branch coverage is informative only -->
<Threshold>100</Threshold>
<!-- Enforce 95% line coverage; branch coverage is informative only -->
<Threshold>95</Threshold>

Copilot uses AI. Check for mistakes.

<ThresholdType>line</ThresholdType>
<ThresholdStat>total</ThresholdStat>
</PropertyGroup>

<!-- Limit coverage to the target assemblies for each test project -->
<PropertyGroup Condition="'$(MSBuildProjectName)' == 'HttpUserAgentParser.UnitTests'" Label="CoverageFilter">
<Include>[MyCSharp.HttpUserAgentParser]*</Include>
</PropertyGroup>
<PropertyGroup Condition="'$(MSBuildProjectName)' == 'HttpUserAgentParser.MemoryCache.UnitTests'" Label="CoverageFilter">
<Include>[MyCSharp.HttpUserAgentParser.MemoryCache]*</Include>
</PropertyGroup>
<PropertyGroup Condition="'$(MSBuildProjectName)' == 'HttpUserAgentParser.AspNetCore.UnitTests'" Label="CoverageFilter">
<Include>[MyCSharp.HttpUserAgentParser.AspNetCore]*</Include>
</PropertyGroup>

<ItemGroup Label="Default Analyzers">
<PackageReference Include="Roslynator.Analyzers">
<PrivateAssets>all</PrivateAssets>
Expand Down
36 changes: 15 additions & 21 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,77 +2,71 @@
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup Label="Default Dependencies">
<PackageVersion Include="NaughtyStrings" Version="2.4.1" />
</ItemGroup>

<ItemGroup Label="Dependencies">
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />

</ItemGroup>

<ItemGroup Label="Libraries for comparison">
<PackageVersion Include="UAParser" Version="3.1.47" />
<PackageVersion Include="DeviceDetector.NET" Version="6.4.2" />
<PackageVersion Include="Ng.UserAgentService" Version="3.0.0" />
</ItemGroup>

<ItemGroup Label="Benchmarks">
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<PackageVersion Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.14.0" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.2" />
<PackageVersion Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.15.2" />
</ItemGroup>

<ItemGroup Label="Tests">
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Testing" Version="8.10.0" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="NSubstitute.Analyzers.CSharp" Version="1.0.17">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageVersion>
<PackageVersion Include="xunit.v3" Version="2.0.1" />
<PackageVersion Include="xunit.v3.extensibility.core" Version="2.0.1" />
<PackageVersion Include="xunit.v3.assert" Version="2.0.1" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" />
<PackageVersion Include="xunit.v3" Version="3.0.1" />
<PackageVersion Include="xunit.v3.extensibility.core" Version="3.0.1" />
<PackageVersion Include="xunit.v3.assert" Version="3.0.1" />
<PackageVersion Include="xunit.runner.console" Version="2.9.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.2">
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
</ItemGroup>

<ItemGroup Label="Analyzers">
<PackageVersion Include="Roslynator.Analyzers" Version="4.13.1">
<PackageVersion Include="Roslynator.Analyzers" Version="4.14.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageVersion>
<PackageVersion Include="Roslynator.Formatting.Analyzers" Version="4.13.1">
<PackageVersion Include="Roslynator.Formatting.Analyzers" Version="4.14.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageVersion>
<PackageVersion Include="Roslynator.CodeAnalysis.Analyzers" Version="4.13.1">
<PackageVersion Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageVersion>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.13.0">
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageVersion>
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.13.0">
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageVersion>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0">
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageVersion>
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.196">
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.212">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageVersion>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright © https://myCSharp.de - all rights reserved

using Microsoft.AspNetCore.Http;
using MyCSharp.HttpUserAgentParser.AspNetCore;
using MyCSharp.HttpUserAgentParser.Providers;
using NSubstitute;
using Xunit;

namespace MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests;

public class HttpContextExtensionsTests
{
[Fact]
public void GetUserAgentString_Returns_Value_When_Present()
{
HttpContext ctx = HttpContextTestHelpers.GetHttpContext("UA");
Assert.Equal("UA", ctx.GetUserAgentString());
}

[Fact]
public void GetUserAgentString_Returns_Null_When_Absent()
{
DefaultHttpContext ctx = new();
Assert.Null(ctx.GetUserAgentString());
}

[Fact]
public void Accessor_Get_Returns_Null_When_Header_Missing()
{
var provider = Substitute.For<IHttpUserAgentParserProvider>();
HttpUserAgentParserAccessor accessor = new(provider);
DefaultHttpContext ctx = new();

Assert.Null(accessor.Get(ctx));
provider.DidNotReceiveWithAnyArgs().Parse(default!);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,11 @@
<ProjectReference Include="..\..\tests\HttpUserAgentParser.TestHelpers\HttpUserAgentParser.TestHelpers.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Update="coverlet.msbuild">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,11 @@
<ProjectReference Include="..\..\src\HttpUserAgentParser.MemoryCache\HttpUserAgentParser.MemoryCache.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Update="coverlet.msbuild">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright © https://myCSharp.de - all rights reserved

using Microsoft.Extensions.Caching.Memory;
using Xunit;

namespace MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests;

public class HttpUserAgentParserMemoryCachedProviderAdditionalTests
{
[Fact]
public void Options_Defaults_Are_Set()
{
HttpUserAgentParserMemoryCachedProviderOptions options = new();
Assert.NotNull(options.CacheOptions);
Assert.NotNull(options.CacheEntryOptions);
Assert.True(options.CacheOptions.SizeLimit is null || options.CacheOptions.SizeLimit >= 0);
Assert.NotEqual(default, options.CacheEntryOptions.SlidingExpiration);
}

[Fact]
public void Provider_Caches_Entries_And_Resolves_Twice()
{
HttpUserAgentParserMemoryCachedProvider provider = new(new HttpUserAgentParserMemoryCachedProviderOptions(new MemoryCacheOptions { SizeLimit = 10 }));
string ua = "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36";
HttpUserAgentInformation a = provider.Parse(ua);
HttpUserAgentInformation b = provider.Parse(ua);

Assert.Equal(a.Name, b.Name);
Assert.Equal(a.Version, b.Version);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,11 @@
<ProjectReference Include="..\..\src\HttpUserAgentParser\HttpUserAgentParser.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Update="coverlet.msbuild">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
75 changes: 75 additions & 0 deletions tests/HttpUserAgentParser.UnitTests/HttpUserAgentParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,79 @@ public void InvalidUserAgent(string userAgent)
Assert.False(info.IsBrowser());
Assert.False(info.IsRobot());
}

[Fact]
public void Cleanup_Trims_Input()
{
string input = " Mozilla/5.0 ";
Assert.Equal("Mozilla/5.0", HttpUserAgentParser.Cleanup(input));
}

[Fact]
public void TryGetPlatform_True_And_False()
{
bool ok = HttpUserAgentParser.TryGetPlatform("Mozilla/5.0 (Windows NT 10.0)", out HttpUserAgentPlatformInformation? platform);
Assert.True(ok);
Assert.NotNull(platform);
Assert.Equal(HttpUserAgentPlatformType.Windows, platform!.Value.PlatformType);

ok = HttpUserAgentParser.TryGetPlatform("UnknownAgent", out platform);
Assert.False(ok);
Assert.Null(platform);
}

[Fact]
public void TryGetRobot_True_And_False()
{
bool ok = HttpUserAgentParser.TryGetRobot("Googlebot/2.1 (+http://www.google.com/bot.html)", out string? robot);
Assert.True(ok);
Assert.Equal("Googlebot", robot);

ok = HttpUserAgentParser.TryGetRobot("NoBotHere", out robot);
Assert.False(ok);
Assert.Null(robot);
}

[Fact]
public void TryGetMobileDevice_True_And_False()
{
bool ok = HttpUserAgentParser.TryGetMobileDevice("(iPhone; CPU iPhone OS)", out string? device);
Assert.True(ok);
Assert.Equal("Apple iPhone", device);

ok = HttpUserAgentParser.TryGetMobileDevice("Desktop Machine", out device);
Assert.False(ok);
Assert.Null(device);
}

[Fact]
public void TryGetBrowser_False_When_Token_Without_Slash()
{
// Contains DetectToken (Edg) but not followed by '/', should be ignored by fast-path and no regex fallback here
(string Name, string? Version)? browser;
bool ok = HttpUserAgentParser.TryGetBrowser("Mozilla Edg 123 something", out browser);
Assert.False(ok);
Assert.Null(browser);
}

[Fact]
public void GetBrowser_Trident_Without_RV_Falls_Back_To_Detect_Token()
{
// Trident present but no rv:, fallback should extract version after DetectToken (Trident/7.0)
(string Name, string? Version)? browser = HttpUserAgentParser.GetBrowser("Mozilla/5.0 (Windows NT 10.0; Win64; x64) Trident/7.0 like Gecko");
Assert.NotNull(browser);
Assert.Equal("Internet Explorer", browser!.Value.Name);
Assert.Equal("7.0", browser.Value.Version);
}

[Fact]
public void GetBrowser_LongToken_NoDigits_Within_Window_Does_Not_Parse_Version()
{
// Build UA: Detect token present (Chrome), but after '/' there are no digits within first 200 chars
string longJunk = new('a', 200);
string ua = $"Mozilla/5.0 Chrome/{longJunk} versionafterwindow1.2";

(string Name, string? Version)? browser = HttpUserAgentParser.GetBrowser(ua);
Assert.Null(browser); // Should fail to extract version and continue, ending with no browser match
}
}