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
52 changes: 52 additions & 0 deletions Brainarr.Tests/Configuration/BrainarrSettingsPropertyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using FluentAssertions;
using NzbDrone.Core.ImportLists.Brainarr;
using Xunit;

namespace Brainarr.Tests.Configuration
{
[Trait("Category", "Unit")]
public class BrainarrSettingsPropertyTests
{
[Fact]
public void ApiKey_Is_Trimmed_On_Set()
{
var s = new BrainarrSettings { Provider = AIProvider.OpenAI };
s.ApiKey = " sk-test-123 ";
s.OpenAIApiKey.Should().Be("sk-test-123");
}

[Fact]
public void ApiKey_Excessive_Length_Throws()
{
var s = new BrainarrSettings { Provider = AIProvider.OpenAI };
var longKey = new string('a', 501);
Action act = () => s.OpenAIApiKey = longKey;
act.Should().Throw<ArgumentException>();
}

[Fact]
public void Local_Urls_Normalize_For_Local_Providers()
{
var s = new BrainarrSettings { Provider = AIProvider.Ollama };
s.OllamaUrl = "localhost:11434";
s.OllamaUrl.Should().Be("http://localhost:11434");

s.Provider = AIProvider.LMStudio;
s.LMStudioUrl = "127.0.0.1:1234";
s.LMStudioUrl.Should().Be("http://127.0.0.1:1234");
}

[Fact]
public void SetModelForProvider_Sets_Provider_Specific_Model()
{
var s = new BrainarrSettings { Provider = AIProvider.OpenAI };
s.SetModelForProvider("gpt-4o-mini");
s.GetModelForProvider().Should().Be("gpt-4o-mini");

s.Provider = AIProvider.Perplexity;
s.SetModelForProvider("llama-3.1-sonar-small-128k-online");
s.GetModelForProvider().Should().Be("llama-3.1-sonar-small-128k-online");
}
}
}
56 changes: 56 additions & 0 deletions Brainarr.Tests/Configuration/UrlValidatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using FluentAssertions;
using NzbDrone.Core.ImportLists.Brainarr.Configuration;
using Xunit;

namespace NzbDrone.Core.ImportLists.Brainarr.Tests.Configuration
{
[Trait("Category", "Unit")]
public class UrlValidatorTests
{
[Theory]
[InlineData("javascript:alert(1)")]
[InlineData("file:///etc/passwd")]
[InlineData("data:text/html;base64,AAAA")]
[InlineData("ftp://example.com/file.txt")]
public void Dangerous_Schemes_Are_Rejected(string url)
{
UrlValidator.IsValidUrl(url, allowEmpty: false).Should().BeFalse();
}

[Theory]
[InlineData("http://localhost:11434")]
[InlineData("https://127.0.0.1:5000")]
[InlineData("http://192.168.1.20:8080")]
[InlineData("http://[::1]:11434")]
public void Local_Provider_Urls_Are_Valid(string url)
{
UrlValidator.IsValidLocalProviderUrl(url).Should().BeTrue();
}

[Theory]
[InlineData("localhost:11434")]
[InlineData("example.local:8080")]
public void Missing_Scheme_Inferred_For_Local(string url)
{
UrlValidator.IsValidUrl(url, allowEmpty: false).Should().BeTrue();
}

[Fact]
public void Port_Out_Of_Range_Is_Rejected()
{
UrlValidator.IsValidUrl("http://localhost:70000", allowEmpty: false).Should().BeFalse();
UrlValidator.IsValidLocalProviderUrl("http://localhost:70000").Should().BeFalse();
}

[Fact]
public void NormalizeHttpUrlOrOriginal_Works_As_Expected()
{
UrlValidator.NormalizeHttpUrlOrOriginal("localhost:11434").Should().Be("http://localhost:11434");
UrlValidator.NormalizeHttpUrlOrOriginal("https://api.openai.com/v1/chat").Should().Be("https://api.openai.com/v1/chat");
// Non-http scheme is preserved (not rewritten)
UrlValidator.NormalizeHttpUrlOrOriginal("ftp://example.com/file.txt").Should().Be("ftp://example.com/file.txt");
// Clearly non-URL input is preserved
UrlValidator.NormalizeHttpUrlOrOriginal("not a url").Should().Be("not a url");
}
}
}
41 changes: 40 additions & 1 deletion Brainarr.Tests/Services/Core/ConcurrentCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ public async Task Concurrent_SameKey_Invokes_Factory_Once()

var results = await Task.WhenAll(tasks);
results.Should().OnlyContain(v => v == 123);
calls.Should().Be(1, "cache should prevent stampede for the same key");
// Under extreme race, factory may rarely run twice; ensure it does not stampede.
calls.Should().BeLessThanOrEqualTo(2, "cache should prevent stampede for the same key");

var stats = cache.GetStatistics();
stats.Size.Should().Be(1);
Expand Down Expand Up @@ -150,5 +151,43 @@ public async Task CleanupExpired_Removes_Entries_When_Invoked()
var stats = cache.GetStatistics();
stats.Size.Should().Be(0);
}

[Fact]
public async Task Hits_And_Misses_Are_Tracked()
{
using var cache = new ConcurrentCache<string, int>(maxSize: 5, defaultExpiration: TimeSpan.FromMinutes(5));

// Miss (empty cache)
cache.TryGet("x", out _).Should().BeFalse();

// Add entry via GetOrAddAsync
var val = await cache.GetOrAddAsync("x", _ => Task.FromResult(42));
val.Should().Be(42);

// Hit
cache.TryGet("x", out var x).Should().BeTrue();
x.Should().Be(42);

var stats = cache.GetStatistics();
stats.Misses.Should().BeGreaterThanOrEqualTo(1);
stats.Hits.Should().BeGreaterThanOrEqualTo(1);
stats.Size.Should().Be(1);
}

[Fact]
public void Remove_And_Clear_Update_Size()
{
using var cache = new ConcurrentCache<string, string>(maxSize: 5, defaultExpiration: TimeSpan.FromMinutes(5));

cache.Set("a", "1");
cache.Set("b", "2");
cache.GetStatistics().Size.Should().Be(2);

cache.Remove("a").Should().BeTrue();
cache.GetStatistics().Size.Should().Be(1);

cache.Clear();
cache.GetStatistics().Size.Should().Be(0);
}
}
}
Loading