Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
63ab32b
fix(build): correct docs URL constants and escape sample curl strings…
Sep 13, 2025
a661b98
ci(projects): skip add-to-project when secret unavailable or on forks…
Sep 13, 2025
8e01e93
test(gemini): fix verbatim JSON string quoting to unblock build
Sep 13, 2025
a8d67c5
style(test): fix indentation flagged by dotnet-format
Sep 13, 2025
b56bbe6
ci: retrigger CI after formatting fixes
Sep 13, 2025
dc3685d
test(gemini): resolve merge conflict and normalize indentation
Sep 13, 2025
11d7598
test(cache): deflake concurrent single-key test by relaxing size asse…
Sep 13, 2025
1207192
chore(pre-commit): normalize EOF and whitespace in scripts/examples t…
Sep 13, 2025
6a65341
docs(api): fill provider action docs; tests: normalize EOF to satisfy…
Sep 13, 2025
a56c35e
style(whitespace): trim trailing spaces in EnhancedRecommendationCach…
Sep 13, 2025
fa0348d
style(whitespace): remove trailing spaces in CacheMetrics methods
Sep 13, 2025
b01f757
style(pre-commit): remove trailing whitespace in tests and cache conf…
Sep 13, 2025
c7d9cf3
style(pre-commit): resolve trailing-whitespace merge conflicts in tests
Sep 13, 2025
3bcc6f1
style(pre-commit): strip whitespace-only lines in EnhancedRecommendat…
Sep 13, 2025
7c800a9
style(pre-commit): trim remaining whitespace-only lines (CacheOptions…
Sep 13, 2025
b2fcb80
style(pre-commit): global trim of trailing whitespace (cache + two te…
Sep 13, 2025
4682410
style(pre-commit): trim trailing whitespace in EnhancedRecommendation…
Sep 13, 2025
5d583c5
style(pre-commit): remove trailing blank lines + spaces (cache + tests)
Sep 13, 2025
6181c85
style(pre-commit): enforce LF endings and strip trailing whitespace (…
Sep 13, 2025
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#if BRAINARR_EXPERIMENTAL_CACHE
#if BRAINARR_EXPERIMENTAL_CACHE
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
Expand Down Expand Up @@ -806,3 +806,4 @@ private void RecordAccessTime(double milliseconds)
}
}
#endif

48 changes: 46 additions & 2 deletions Brainarr.Tests/RateLimiting/EnhancedRateLimiterTests.cs
Original file line number Diff line number Diff line change
@@ -1,54 +1,98 @@
using System;
using System;

using System.Net;

using System.Threading.Tasks;

using NLog;

using NzbDrone.Core.ImportLists.Brainarr.Services.RateLimiting;

using Xunit;


namespace Brainarr.Tests.RateLimiting

{

public class EnhancedRateLimiterTests

{

private static Logger TestLogger => LogManager.GetCurrentClassLogger();


[Fact]

public async Task Allows_first_request_then_blocks_second_with_tight_policy()

{

var limiter = new EnhancedRateLimiter(TestLogger);

limiter.ConfigureLimit("test", new RateLimitPolicy

{

MaxRequests = 1,

Period = TimeSpan.FromSeconds(5),

EnableUserLimit = false,

EnableIpLimit = false,

EnableResourceLimit = true

});


var req = new RateLimitRequest { Resource = "test", UserId = "u1", IpAddress = IPAddress.Loopback };


var first = await limiter.CheckRateLimitAsync(req);

Assert.True(first.IsAllowed);


// Consume and then check again

await limiter.ExecuteAsync(req, async () => 1);


var second = await limiter.CheckRateLimitAsync(req);

Assert.False(second.IsAllowed);

Assert.True(second.RetryAfter.HasValue);

}


[Fact]

public void TokenBucket_refills_over_time()

{

var bucket = new TokenBucket(2, TimeSpan.FromMilliseconds(200));

Assert.True(bucket.TryConsume());

Assert.True(bucket.TryConsume());

Assert.False(bucket.TryConsume());


// Wait for refill

var wait = bucket.GetWaitTime(1);

Assert.True(wait > TimeSpan.Zero);

}

}

}

70 changes: 68 additions & 2 deletions Brainarr.Tests/Services/Core/ModelActionHandlerDetailsTests.cs
Original file line number Diff line number Diff line change
@@ -1,75 +1,141 @@
using System;
using System;

using System.Collections.Generic;

using System.Threading.Tasks;

using FluentAssertions;

using NLog;

using NzbDrone.Common.Http;

using NzbDrone.Core.ImportLists.Brainarr;

using NzbDrone.Core.ImportLists.Brainarr.Configuration;

using NzbDrone.Core.ImportLists.Brainarr.Models;

using NzbDrone.Core.ImportLists.Brainarr.Services;

using NzbDrone.Core.ImportLists.Brainarr.Services.Core;

using Xunit;


namespace Brainarr.Tests.Services.Core

{

public class ModelActionHandlerDetailsTests

{

private class FakeProviderWithHint : IAIProvider

{

public string ProviderName => "Google Gemini";

public Task<List<Recommendation>> GetRecommendationsAsync(string prompt) => Task.FromResult(new List<Recommendation>());

public Task<bool> TestConnectionAsync() => Task.FromResult(false);

public void UpdateModel(string modelName) { }

public string? GetLastUserMessage() => "Gemini API disabled for this key's Google Cloud project. Enable the Generative Language API: https://console.developers.google.com/apis/api/generativelanguage.googleapis.com/overview?project=123";

}


private class FakeProviderFactory : IProviderFactory

{

private readonly IAIProvider _provider;

public FakeProviderFactory(IAIProvider provider) { _provider = provider; }

public IAIProvider CreateProvider(BrainarrSettings settings, IHttpClient httpClient, Logger logger) => _provider;

public bool IsProviderAvailable(AIProvider providerType, BrainarrSettings settings) => true;

}


private class NoopHttpClient : IHttpClient

{

public Task<HttpResponse> ExecuteAsync(HttpRequest request) => Task.FromResult(new HttpResponse(request, new HttpHeader(), "{}"));

public HttpResponse Execute(HttpRequest request) => new HttpResponse(request, new HttpHeader(), "{}");

public void DownloadFile(string url, string fileName) => throw new NotImplementedException();

public Task DownloadFileAsync(string url, string fileName) => throw new NotImplementedException();

public HttpResponse Get(HttpRequest request) => Execute(request);

public Task<HttpResponse> GetAsync(HttpRequest request) => ExecuteAsync(request);

public HttpResponse<T> Get<T>(HttpRequest request) where T : new() => throw new NotImplementedException();

public Task<HttpResponse<T>> GetAsync<T>(HttpRequest request) where T : new() => throw new NotImplementedException();

public HttpResponse Head(HttpRequest request) => Execute(request);

public Task<HttpResponse> HeadAsync(HttpRequest request) => ExecuteAsync(request);

public HttpResponse Post(HttpRequest request) => Execute(request);

public Task<HttpResponse> PostAsync(HttpRequest request) => ExecuteAsync(request);

public HttpResponse<T> Post<T>(HttpRequest request) where T : new() => throw new NotImplementedException();

public Task<HttpResponse<T>> PostAsync<T>(HttpRequest request) where T : new() => throw new NotImplementedException();

}


private static Logger L => LogManager.GetCurrentClassLogger();


[Fact]

public async Task HandleTestConnectionDetailsAsync_surfaces_hint_on_failure()

{

var settings = new BrainarrSettings { Provider = AIProvider.Gemini };

var provider = new FakeProviderWithHint();

var handler = new ModelActionHandler(

new ModelDetectionService(new NoopHttpClient(), L),

new FakeProviderFactory(provider),

new NoopHttpClient(),

L);


var result = await handler.HandleTestConnectionDetailsAsync(settings);


result.Success.Should().BeFalse();

result.Provider.Should().Be("Google Gemini");

result.Hint.Should().NotBeNull();

result.Hint.Should().Contain("Enable the Generative Language API");

}

}

}