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(() => 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(() => 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(() => 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))); } }