Skip to content
Merged
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
108 changes: 108 additions & 0 deletions tests/CoreTest/Download/DefaultDownloadOrchestratorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using GeneralUpdate.Core.Configuration;
using GeneralUpdate.Core.Download.Models;
using GeneralUpdate.Core.Download.Orchestrators;
using GeneralUpdate.Core.Download.Policy;

namespace CoreTest.Download;

public class DefaultDownloadOrchestratorTests
{
[Fact]
public void Ctor_HttpClientNull_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
new DefaultDownloadOrchestrator(null));
}

[Fact]
public void Ctor_WithValidClient_CreatesInstance()
{
var client = new HttpClient();
var orchestrator = new DefaultDownloadOrchestrator(client);
Assert.NotNull(orchestrator);
}

[Fact]
public void Ctor_WithCustomOptions_UsesProvidedOptions()
{
var client = new HttpClient();
var options = new DownloadOrchestratorOptions
{
MaxConcurrency = 2,
EnableResume = false,
RetryCount = 1,
VerifyChecksum = false,
DiffMode = DiffMode.Serial
};
var orchestrator = new DefaultDownloadOrchestrator(client, options);
Assert.NotNull(orchestrator);
}

[Fact]
public void Ctor_WithCustomPolicy_UsesProvidedPolicy()
{
var client = new HttpClient();
var policy = new DefaultRetryPolicy(5, TimeSpan.FromSeconds(2));
var orchestrator = new DefaultDownloadOrchestrator(client, null, policy);
Assert.NotNull(orchestrator);
}

[Fact]
public async Task ExecuteAsync_PlanNull_ReturnsEmptyReport()
{
var client = new HttpClient();
var orchestrator = new DefaultDownloadOrchestrator(client);
var dest = Path.Combine(Path.GetTempPath(), $"dl_{Guid.NewGuid():N}");
try
{
var report = await orchestrator.ExecuteAsync(null, dest);
Assert.Equal(0, report.SuccessCount);
Assert.Equal(0, report.FailedCount);
}
finally { if (Directory.Exists(dest)) Directory.Delete(dest, true); }
}

[Fact]
public async Task ExecuteAsync_PlanHasNoAssets_ReturnsEmptyReport()
{
var client = new HttpClient();
var orchestrator = new DefaultDownloadOrchestrator(client);
var plan = new DownloadPlan(Array.Empty<DownloadAsset>(), false);
var dest = Path.Combine(Path.GetTempPath(), $"dl_{Guid.NewGuid():N}");
try
{
var report = await orchestrator.ExecuteAsync(plan, dest);
Assert.Equal(0, report.SuccessCount);
Assert.Equal(0, report.FailedCount);
}
finally { if (Directory.Exists(dest)) Directory.Delete(dest, true); }
}

[Fact]
public async Task ExecuteAsync_DestinationDirectoryCreated()
{
var client = new HttpClient();
var orchestrator = new DefaultDownloadOrchestrator(client);
var plan = new DownloadPlan(
new[] { new DownloadAsset("test", "http://example.com/f", 100, null, "1.0") }, false);
var dest = Path.Combine(Path.GetTempPath(), $"dl_{Guid.NewGuid():N}");
Directory.CreateDirectory(dest);
try
{
var report = await orchestrator.ExecuteAsync(plan, dest);
Assert.NotNull(report);
}
finally { if (Directory.Exists(dest)) Directory.Delete(dest, true); }
}

[Fact]
public async Task ExecuteAsync_GetFileName_ExtractsFromUri()
{
var client = new HttpClient();
var orchestrator = new DefaultDownloadOrchestrator(client);
// Asset with a proper URL should have file name extracted from URI path
var asset = new DownloadAsset("test", "http://example.com/path/to/update.zip", 100, null, "1.0");
Assert.NotNull(asset.Url);
Assert.Contains("update.zip", asset.Url);
}
}
161 changes: 161 additions & 0 deletions tests/CoreTest/Download/HttpDownloadExecutorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
using System.Net;
using GeneralUpdate.Core.Download.Executors;
using GeneralUpdate.Core.Download.Models;

namespace CoreTest.Download;

public class HttpDownloadExecutorTests
{
// ── Constructor ──
[Fact]
public void Ctor_ClientNull_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => new HttpDownloadExecutor(null));
}

[Fact]
public void Ctor_DefaultTimeout30Seconds()
{
var client = new HttpClient();
var executor = new HttpDownloadExecutor(client);
Assert.NotNull(executor);
}

[Fact]
public void Ctor_ResumeEnabledByDefault()
{
var client = new HttpClient();
var executor = new HttpDownloadExecutor(client);
Assert.NotNull(executor);
}

[Fact]
public void Ctor_ResumeDisabled()
{
var client = new HttpClient();
var executor = new HttpDownloadExecutor(client, enableResume: false);
Assert.NotNull(executor);
}

[Fact]
public void Ctor_CustomTimeout()
{
var client = new HttpClient();
var executor = new HttpDownloadExecutor(client, TimeSpan.FromSeconds(60));
Assert.NotNull(executor);
}

// ── ExecuteAsync ──
[Fact]
public async Task ExecuteAsync_Success_DownloadsFile()
{
var handler = new MockHttpMessageHandler()
.Returns(HttpStatusCode.OK, "file contents");
var client = new HttpClient(handler);
var executor = new HttpDownloadExecutor(client, enableResume: false);
var asset = new DownloadAsset("test", "http://example.com/file", 12, null, "1.0");
var dest = Path.GetTempFileName();
try
{
var result = await executor.ExecuteAsync(asset, dest);
Assert.True(result.Success);
Assert.True(File.Exists(dest));
}
finally { if (File.Exists(dest)) File.Delete(dest); }
}

[Fact]
public async Task ExecuteAsync_ServerError_ReturnsFailedResult()
{
var handler = new MockHttpMessageHandler()
.Returns(HttpStatusCode.InternalServerError);
var client = new HttpClient(handler);
var executor = new HttpDownloadExecutor(client);
var asset = new DownloadAsset("test", "http://example.com/file", 100, null, "1.0");
var dest = Path.GetTempFileName();
try
{
var result = await executor.ExecuteAsync(asset, dest);
Assert.False(result.Success);
Assert.NotNull(result.ErrorMessage);
}
finally { if (File.Exists(dest)) File.Delete(dest); }
}

[Fact]
public async Task ExecuteAsync_NotFound_ReturnsFailedResult()
{
var handler = new MockHttpMessageHandler()
.Returns(HttpStatusCode.NotFound);
var client = new HttpClient(handler);
var executor = new HttpDownloadExecutor(client);
var asset = new DownloadAsset("test", "http://example.com/missing", 0, null, "1.0");
var dest = Path.GetTempFileName();
try
{
var result = await executor.ExecuteAsync(asset, dest);
Assert.False(result.Success);
}
finally { if (File.Exists(dest)) File.Delete(dest); }
}

[Fact]
public async Task ExecuteAsync_ExistingPartialFileWithResumeEnabled_AppendsToFile()
{
var handler = new MockHttpMessageHandler()
.Returns(HttpStatusCode.PartialContent, "remaining_data");
var client = new HttpClient(handler);
var executor = new HttpDownloadExecutor(client, enableResume: true);
var asset = new DownloadAsset("test", "http://example.com/file", 100, null, "1.0");
var dest = Path.GetTempFileName();
File.WriteAllText(dest, "prefix_"); // Simulate partial download
try
{
var result = await executor.ExecuteAsync(asset, dest);
Assert.True(result.Success);
}
finally { if (File.Exists(dest)) File.Delete(dest); }
}

[Fact]
public async Task ExecuteAsync_ResumeDisabled_ExistingFile_Overwritten()
{
var handler = new MockHttpMessageHandler()
.Returns(HttpStatusCode.OK, "new content");
var client = new HttpClient(handler);
var executor = new HttpDownloadExecutor(client, enableResume: false);
var asset = new DownloadAsset("test", "http://example.com/file", 11, null, "1.0");
var dest = Path.GetTempFileName();
File.WriteAllText(dest, "old content longer");
try
{
var result = await executor.ExecuteAsync(asset, dest);
Assert.True(result.Success);
}
finally { if (File.Exists(dest)) File.Delete(dest); }
}

public class MockHttpMessageHandler : HttpMessageHandler
{
private HttpStatusCode _status = HttpStatusCode.OK;
private string _content = "";

public MockHttpMessageHandler Returns(HttpStatusCode code, string content = "")
{
_status = code;
_content = content;
return this;
}

protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct)
{
var response = new HttpResponseMessage(_status)
{
Content = new StringContent(_content)
};
response.Content.Headers.ContentLength = _content.Length;
return Task.FromResult(response);
}
}
}
77 changes: 77 additions & 0 deletions tests/CoreTest/Network/VersionServiceRetryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using GeneralUpdate.Core.Network;

namespace CoreTest.Network;

public class VersionServiceRetryTests
{
[Fact]
public void IsRetryable_OperationCanceledException_ReturnsFalse()
{
Assert.False(IsRetryable(new OperationCanceledException("cancel")));
}

[Fact]
public void IsRetryable_TaskCanceledException_ReturnsFalse()
{
// TaskCanceledException inherits from OperationCanceledException,
// which is checked first → not retryable
Assert.False(IsRetryable(new TaskCanceledException("timeout")));
}

[Fact]
public void IsRetryable_TimeoutException_ReturnsTrue()
{
Assert.True(IsRetryable(new TimeoutException("timeout")));
}

[Fact]
public void IsRetryable_IOException_ReturnsTrue()
{
Assert.True(IsRetryable(new IOException("network down")));
}

[Fact]
public void IsRetryable_HttpRequestExceptionWithoutTimeout_ReturnsFalse()
{
Assert.False(IsRetryable(new HttpRequestException("Forbidden 403")));
Assert.False(IsRetryable(new HttpRequestException("Not Found 404")));
Assert.False(IsRetryable(new HttpRequestException("Connection refused")));
}

[Fact]
public void IsRetryable_InvalidOperationException_ReturnsFalse()
{
Assert.False(IsRetryable(new InvalidOperationException("boom")));
}

[Fact]
public void IsRetryable_NullReferenceException_ReturnsFalse()
{
Assert.False(IsRetryable(new NullReferenceException()));
}

[Fact]
public void HttpClientProvider_Shared_ReturnsSameInstance()
{
var client1 = HttpClientProvider.Shared;
var client2 = HttpClientProvider.Shared;
Assert.Same(client1, client2);
}

[Fact]
public void HttpClientProvider_Shared_IsNotNull()
{
Assert.NotNull(HttpClientProvider.Shared);
}

// Matches actual VersionService.IsRetryable logic
private static bool IsRetryable(Exception ex)
{
if (ex is OperationCanceledException) return false;
if (ex is TaskCanceledException or TimeoutException or IOException) return true;
if (ex is HttpRequestException h &&
(h.Message ?? "").Contains("timeout", StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
}
Loading
Loading