Files
jellyfin-multilang/tests/Jellyfin.Plugin.Multilang.Tests/MaintenanceAndProviderTests.cs
T
ajp_anton e36775f1a3 Release 0.2.5: harden proxy and streamline metadata fetching
Remove obsolete rule formats, batch SQLite work, preserve artwork source URLs, and coordinate refresh workers with maintenance. Add regression coverage and document backup format changes.
2026-09-09 01:18:43 +00:00

100 lines
3.9 KiB
C#

using System.Diagnostics;
using System.Net;
using Jellyfin.Plugin.Multilang.Data;
using Jellyfin.Plugin.Multilang.Services.Providers;
namespace Jellyfin.Plugin.Multilang.Tests;
public sealed class MaintenanceAndProviderTests
{
[Fact]
public async Task ConcurrentRefreshesCanOverlapButMaintenanceWaitsForAllOfThem()
{
var root = TestPaths.CreateRoot();
try
{
var store = new TranslationStore(new TestApplicationPaths(root));
var first = await store.EnterRefreshAsync(CancellationToken.None);
var second = await store.EnterRefreshAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(5));
using var cancel = new CancellationTokenSource();
var maintenance = store.EnterMaintenanceAsync(cancel.Token);
Assert.False(maintenance.IsCompleted);
first.Dispose();
Assert.False(maintenance.IsCompleted);
cancel.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => maintenance);
second.Dispose();
using var allSlots = await store.EnterMaintenanceAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(5));
}
finally { TestPaths.DeleteRoot(root); }
}
[Fact]
public async Task MaintenanceWaitsForWriterAndCanceledWaiterDoesNotLeakLease()
{
var root = TestPaths.CreateRoot();
try
{
var store = new TranslationStore(new TestApplicationPaths(root));
var writer = await store.EnterMaintenanceAsync(CancellationToken.None);
using var canceled = new CancellationTokenSource();
var waiting = store.EnterMaintenanceAsync(canceled.Token);
Assert.False(waiting.IsCompleted);
canceled.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => waiting);
var cleanup = store.EnterMaintenanceAsync(CancellationToken.None);
Assert.False(cleanup.IsCompleted);
writer.Dispose();
using var lease = await cleanup.WaitAsync(TimeSpan.FromSeconds(5));
}
finally { TestPaths.DeleteRoot(root); }
}
[Fact]
public void ResetKeepsExistingConnectionsOnTheSameDatabase()
{
var root = TestPaths.CreateRoot();
try
{
var store = new TranslationStore(new TestApplicationPaths(root));
store.SaveUserRules("user", new UserRulesDocument());
using var connection = store.Open();
connection.Open();
using var query = connection.CreateCommand();
query.CommandText = "SELECT COUNT(*) FROM user_rules;";
Assert.Equal(1L, query.ExecuteScalar());
store.ResetAll();
Assert.Equal(0L, query.ExecuteScalar());
}
finally { TestPaths.DeleteRoot(root); }
}
[Fact]
public async Task ProviderDoesNotRetryProgrammingErrors()
{
var attempts = 0;
using var http = new HttpClient(new TestHttpMessageHandler(_ =>
{
attempts++;
throw new InvalidOperationException("Programming error");
}));
using var request = new HttpRequestMessage(HttpMethod.Get, "https://example.test/");
await Assert.ThrowsAsync<InvalidOperationException>(() => ProviderHttp.SendWithRetryAsync(http, request, CancellationToken.None));
Assert.Equal(1, attempts);
}
[Fact]
public async Task ConcurrentProviderAdmissionsKeepTheirSpacing()
{
var limiter = new FetchRateLimiter(TimeSpan.FromMilliseconds(50));
var starts = await Task.WhenAll(Enumerable.Range(0, 4).Select(async _ =>
{
await limiter.WaitAsync(CancellationToken.None);
return Stopwatch.GetTimestamp();
}));
Array.Sort(starts);
Assert.All(starts.Zip(starts.Skip(1)), pair =>
Assert.True(Stopwatch.GetElapsedTime(pair.First, pair.Second) >= TimeSpan.FromMilliseconds(40)));
}
}