From 962cb49a7e0eabefc1a76e630d333cb652eb0428 Mon Sep 17 00:00:00 2001 From: Benjamin Abt Date: Mon, 25 Aug 2025 13:22:03 +0200 Subject: [PATCH] feat(tests): add coverage settings and new unit tests --- Directory.Build.props | 26 +++++++ Directory.Packages.props | 36 ++++----- .../HttpContextExtensionsTests.cs | 37 +++++++++ ...serAgentParser.AspNetCore.UnitTests.csproj | 7 ++ ...erAgentParser.MemoryCache.UnitTests.csproj | 7 ++ ...rserMemoryCachedProviderAdditionalTests.cs | 31 ++++++++ .../HttpUserAgentParser.UnitTests.csproj | 7 ++ .../HttpUserAgentParserTests.cs | 75 +++++++++++++++++++ 8 files changed, 205 insertions(+), 21 deletions(-) create mode 100644 tests/HttpUserAgentParser.AspNetCore.UnitTests/HttpContextExtensionsTests.cs create mode 100644 tests/HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParserMemoryCachedProviderAdditionalTests.cs diff --git a/Directory.Build.props b/Directory.Build.props index 749af9f..66c247a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -68,6 +68,7 @@ + @@ -88,6 +89,31 @@ + + + true + lcov,opencover,cobertura + $(MSBuildThisFileDirectory)TestResults/coverage/$(MSBuildProjectName). + GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute + **/*Program.cs;**/*Startup.cs;**/*GlobalUsings.cs + true + + 100 + line + total + + + + + [MyCSharp.HttpUserAgentParser]* + + + [MyCSharp.HttpUserAgentParser.MemoryCache]* + + + [MyCSharp.HttpUserAgentParser.AspNetCore]* + + all diff --git a/Directory.Packages.props b/Directory.Packages.props index eaa97d7..ef3e22e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,77 +2,71 @@ true - - - - - - - + + - - + all runtime; build; native; contentfiles; analyzers - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/HttpUserAgentParser.AspNetCore.UnitTests/HttpContextExtensionsTests.cs b/tests/HttpUserAgentParser.AspNetCore.UnitTests/HttpContextExtensionsTests.cs new file mode 100644 index 0000000..065cfb7 --- /dev/null +++ b/tests/HttpUserAgentParser.AspNetCore.UnitTests/HttpContextExtensionsTests.cs @@ -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(); + HttpUserAgentParserAccessor accessor = new(provider); + DefaultHttpContext ctx = new(); + + Assert.Null(accessor.Get(ctx)); + provider.DidNotReceiveWithAnyArgs().Parse(default!); + } +} diff --git a/tests/HttpUserAgentParser.AspNetCore.UnitTests/HttpUserAgentParser.AspNetCore.UnitTests.csproj b/tests/HttpUserAgentParser.AspNetCore.UnitTests/HttpUserAgentParser.AspNetCore.UnitTests.csproj index 269119a..f24713d 100644 --- a/tests/HttpUserAgentParser.AspNetCore.UnitTests/HttpUserAgentParser.AspNetCore.UnitTests.csproj +++ b/tests/HttpUserAgentParser.AspNetCore.UnitTests/HttpUserAgentParser.AspNetCore.UnitTests.csproj @@ -13,4 +13,11 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/tests/HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParser.MemoryCache.UnitTests.csproj b/tests/HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParser.MemoryCache.UnitTests.csproj index b68d12f..4c330fe 100644 --- a/tests/HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParser.MemoryCache.UnitTests.csproj +++ b/tests/HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParser.MemoryCache.UnitTests.csproj @@ -13,4 +13,11 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/tests/HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParserMemoryCachedProviderAdditionalTests.cs b/tests/HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParserMemoryCachedProviderAdditionalTests.cs new file mode 100644 index 0000000..45e9753 --- /dev/null +++ b/tests/HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParserMemoryCachedProviderAdditionalTests.cs @@ -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); + } +} diff --git a/tests/HttpUserAgentParser.UnitTests/HttpUserAgentParser.UnitTests.csproj b/tests/HttpUserAgentParser.UnitTests/HttpUserAgentParser.UnitTests.csproj index cdf4e3d..7366e0c 100644 --- a/tests/HttpUserAgentParser.UnitTests/HttpUserAgentParser.UnitTests.csproj +++ b/tests/HttpUserAgentParser.UnitTests/HttpUserAgentParser.UnitTests.csproj @@ -12,4 +12,11 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/tests/HttpUserAgentParser.UnitTests/HttpUserAgentParserTests.cs b/tests/HttpUserAgentParser.UnitTests/HttpUserAgentParserTests.cs index b778535..972f42a 100644 --- a/tests/HttpUserAgentParser.UnitTests/HttpUserAgentParserTests.cs +++ b/tests/HttpUserAgentParser.UnitTests/HttpUserAgentParserTests.cs @@ -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 + } }