Skip to content

Commit 7ffd24d

Browse files
**Add comprehensive unit tests for services, models, and utilities**
- Added tests for UpdateService, StatsService, BugReportService, AppConfig, PbpExtractionResult, and GitHubRelease - Expanded PathUtilsTests, FileExtensionsTests, and GameFileParserTests with new edge cases and validations - Added InternalsVisibleTo for test access and introduced AppHttpClient - Updated project file and test coverage
1 parent 7e9d86b commit 7ffd24d

23 files changed

Lines changed: 1059 additions & 181 deletions
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System.Runtime.InteropServices;
2+
3+
namespace BatchConvertToCHD.Tests;
4+
5+
public class AppConfigTests
6+
{
7+
[Fact]
8+
public void IsArm64ReturnsBool()
9+
{
10+
var result = AppConfig.IsArm64;
11+
var expected = RuntimeInformation.ProcessArchitecture == Architecture.Arm64;
12+
Assert.Equal(expected, result);
13+
}
14+
15+
[Fact]
16+
public void ChdmanExeNameMatchesArchitecture()
17+
{
18+
if (AppConfig.IsArm64)
19+
{
20+
Assert.Equal("chdman_arm64.exe", AppConfig.ChdmanExeName);
21+
}
22+
else
23+
{
24+
Assert.Equal("chdman.exe", AppConfig.ChdmanExeName);
25+
}
26+
}
27+
28+
[Fact]
29+
public void BugReportApiUrlIsNotEmpty()
30+
{
31+
Assert.False(string.IsNullOrEmpty(AppConfig.BugReportApiUrl));
32+
}
33+
34+
[Fact]
35+
public void BugReportApiKeyIsNotEmpty()
36+
{
37+
Assert.False(string.IsNullOrEmpty(AppConfig.BugReportApiKey));
38+
}
39+
40+
[Fact]
41+
public void ApplicationStatsApiUrlIsNotEmpty()
42+
{
43+
Assert.False(string.IsNullOrEmpty(AppConfig.ApplicationStatsApiUrl));
44+
}
45+
46+
[Fact]
47+
public void ApplicationStatsApiKeyIsNotEmpty()
48+
{
49+
Assert.False(string.IsNullOrEmpty(AppConfig.ApplicationStatsApiKey));
50+
}
51+
52+
[Fact]
53+
public void ApplicationNameIsCorrect()
54+
{
55+
Assert.Equal("BatchConvertToCHD", AppConfig.ApplicationName);
56+
}
57+
58+
[Fact]
59+
public void WriteSpeedUpdateIntervalMsIsPositive()
60+
{
61+
Assert.True(AppConfig.WriteSpeedUpdateIntervalMs > 0);
62+
}
63+
64+
[Fact]
65+
public void MaxConversionTimeoutHoursIsPositive()
66+
{
67+
Assert.True(AppConfig.MaxConversionTimeoutHours > 0);
68+
}
69+
70+
[Fact]
71+
public void PsxPackagerExeNameIsNotEmpty()
72+
{
73+
Assert.False(string.IsNullOrEmpty(AppConfig.PsxPackagerExeName));
74+
}
75+
}

BatchConvertToCHD.Tests/ArchiveServiceTests.cs

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using System.IO.Compression;
2-
using System.Reflection;
32
using BatchConvertToCHD.Services;
3+
using SharpCompressZipArchive = SharpCompress.Archives.Zip.ZipArchive;
44

55
namespace BatchConvertToCHD.Tests;
66

@@ -115,7 +115,7 @@ public async Task ExtractCsoAsyncMaxCsoNotAvailableReturnsFailure()
115115
var service = new ArchiveService("maxcso.exe", false);
116116
var tempIso = Path.Combine(_tempDir, "out.iso");
117117

118-
var result = await service.ExtractCsoAsync("input.cso", tempIso, _tempDir, static _ => { }, static _ => { }, CancellationToken.None);
118+
var result = await service.ExtractCsoAsync("input.cso", tempIso, _tempDir, static _ => { }, CancellationToken.None);
119119

120120
Assert.False(result.Success);
121121
Assert.Contains("maxcso.exe is not available", result.ErrorMessage);
@@ -132,34 +132,50 @@ public void DisposeDoesNotThrow()
132132
[Fact]
133133
public void ExtractArchiveWithFallbackMissingFileThrowsFileNotFoundWithoutFallback()
134134
{
135-
var method = typeof(ArchiveService).GetMethod("ExtractArchiveWithFallback", BindingFlags.NonPublic | BindingFlags.Static);
136-
Assert.NotNull(method);
137-
138-
// Construct the generic method with a concrete archive type that satisfies the constraints
139-
var genericMethod = method.MakeGenericMethod(typeof(SharpCompress.Archives.Zip.ZipArchive));
140-
141135
var missingPath = Path.Combine(_tempDir, $"missing_{Guid.NewGuid():N}.7z");
142136
var outputDir = Path.Combine(_tempDir, "out");
143137
Directory.CreateDirectory(outputDir);
144138
var logs = new List<string>();
145139

146140
var ex = Record.Exception(() =>
147141
{
148-
genericMethod.Invoke(
149-
null,
150-
[
151-
missingPath,
152-
outputDir,
153-
(Action<string>)(logs.Add),
154-
".7z",
155-
(Func<Stream, SharpCompress.Archives.Zip.ZipArchive>)(static _ => throw new InvalidOperationException("Should not reach here")),
156-
CancellationToken.None
157-
]);
142+
ArchiveService.ExtractArchiveWithFallback<SharpCompressZipArchive>(
143+
missingPath,
144+
outputDir,
145+
logs.Add,
146+
".7z",
147+
static _ => throw new InvalidOperationException("Should not reach here"),
148+
CancellationToken.None);
158149
});
159150

160151
Assert.NotNull(ex);
161-
var inner = ex is TargetInvocationException tie ? tie.InnerException : ex;
162-
Assert.IsType<FileNotFoundException>(inner);
152+
Assert.IsType<FileNotFoundException>(ex);
163153
Assert.Contains(logs, static msg => msg.Contains("Skipping fallback", StringComparison.OrdinalIgnoreCase));
164154
}
155+
156+
[Fact]
157+
public async Task ExtractArchiveAsyncInvalidSevenZipReturnsFailure()
158+
{
159+
var service = new ArchiveService("maxcso.exe", false);
160+
var sevenZPath = Path.Combine(_tempDir, "invalid.7z");
161+
File.WriteAllText(sevenZPath, "not a valid 7z archive");
162+
var tempDir = Path.Combine(_tempDir, "extract");
163+
164+
var result = await service.ExtractArchiveAsync(sevenZPath, tempDir, static _ => { }, CancellationToken.None);
165+
166+
Assert.False(result.Success);
167+
}
168+
169+
[Fact]
170+
public async Task ExtractArchiveAsyncInvalidRarReturnsFailure()
171+
{
172+
var service = new ArchiveService("maxcso.exe", false);
173+
var rarPath = Path.Combine(_tempDir, "invalid.rar");
174+
File.WriteAllText(rarPath, "not a valid rar archive");
175+
var tempDir = Path.Combine(_tempDir, "extract");
176+
177+
var result = await service.ExtractArchiveAsync(rarPath, tempDir, static _ => { }, CancellationToken.None);
178+
179+
Assert.False(result.Success);
180+
}
165181
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
using System.Reflection;
2+
using System.Text;
3+
using BatchConvertToCHD.Services;
4+
5+
namespace BatchConvertToCHD.Tests;
6+
7+
public class BugReportServiceTests
8+
{
9+
private const string TestApiUrl = "https://example.com/api/bugreport";
10+
private const string TestApiKey = "test-api-key";
11+
private const string TestAppName = "TestApp";
12+
13+
[Fact]
14+
public void ConstructorStoresParametersCorrectly()
15+
{
16+
var service = new BugReportService(TestApiUrl, TestApiKey, TestAppName);
17+
18+
var apiUrlField = typeof(BugReportService).GetField("_apiUrl", BindingFlags.NonPublic | BindingFlags.Instance);
19+
var apiKeyField = typeof(BugReportService).GetField("_apiKey", BindingFlags.NonPublic | BindingFlags.Instance);
20+
var appNameField = typeof(BugReportService).GetField("_applicationName", BindingFlags.NonPublic | BindingFlags.Instance);
21+
22+
Assert.NotNull(apiUrlField);
23+
Assert.NotNull(apiKeyField);
24+
Assert.NotNull(appNameField);
25+
Assert.Equal(TestApiUrl, apiUrlField.GetValue(service));
26+
Assert.Equal(TestApiKey, apiKeyField.GetValue(service));
27+
Assert.Equal(TestAppName, appNameField.GetValue(service));
28+
}
29+
30+
[Fact]
31+
public void BuildFormattedReportIncludesMessageAndAppName()
32+
{
33+
var service = new BugReportService(TestApiUrl, TestApiKey, TestAppName);
34+
var method = typeof(BugReportService).GetMethod("BuildFormattedReport", BindingFlags.NonPublic | BindingFlags.Instance);
35+
Assert.NotNull(method);
36+
37+
var result = method.Invoke(service, ["Test error message", null]) as string;
38+
Assert.NotNull(result);
39+
Assert.Contains("Test error message", result, StringComparison.Ordinal);
40+
Assert.Contains(TestAppName, result, StringComparison.Ordinal);
41+
}
42+
43+
[Fact]
44+
public void BuildFormattedReportIncludesExceptionDetails()
45+
{
46+
var service = new BugReportService(TestApiUrl, TestApiKey, TestAppName);
47+
var method = typeof(BugReportService).GetMethod("BuildFormattedReport", BindingFlags.NonPublic | BindingFlags.Instance);
48+
Assert.NotNull(method);
49+
50+
var ex = new InvalidOperationException("Something went wrong");
51+
var result = method.Invoke(service, ["Error summary", ex]) as string;
52+
Assert.NotNull(result);
53+
Assert.Contains("Error summary", result, StringComparison.Ordinal);
54+
Assert.Contains("InvalidOperationException", result, StringComparison.Ordinal);
55+
Assert.Contains("Something went wrong", result, StringComparison.Ordinal);
56+
}
57+
58+
[Fact]
59+
public void BuildFormattedReportIncludesInnerException()
60+
{
61+
var service = new BugReportService(TestApiUrl, TestApiKey, TestAppName);
62+
var method = typeof(BugReportService).GetMethod("BuildFormattedReport", BindingFlags.NonPublic | BindingFlags.Instance);
63+
Assert.NotNull(method);
64+
65+
var inner = new ArgumentException("Inner error");
66+
var outer = new InvalidOperationException("Outer error", inner);
67+
var result = method.Invoke(service, ["Error summary", outer]) as string;
68+
Assert.NotNull(result);
69+
Assert.Contains("Inner Exception", result, StringComparison.Ordinal);
70+
Assert.Contains("Inner error", result, StringComparison.Ordinal);
71+
}
72+
73+
[Fact]
74+
public void GetExceptionStackTraceNullReturnsNa()
75+
{
76+
var method = typeof(BugReportService).GetMethod("GetExceptionStackTrace", BindingFlags.NonPublic | BindingFlags.Static);
77+
Assert.NotNull(method);
78+
79+
var result = method.Invoke(null, [null]) as string;
80+
Assert.Equal("N/A", result);
81+
}
82+
83+
[Fact]
84+
public void GetExceptionStackTraceIncludesExceptionDetails()
85+
{
86+
var method = typeof(BugReportService).GetMethod("GetExceptionStackTrace", BindingFlags.NonPublic | BindingFlags.Static);
87+
Assert.NotNull(method);
88+
89+
var ex = new InvalidOperationException("Test error");
90+
var result = method.Invoke(null, [ex]) as string;
91+
Assert.NotNull(result);
92+
Assert.Contains("InvalidOperationException", result, StringComparison.Ordinal);
93+
Assert.Contains("Test error", result, StringComparison.Ordinal);
94+
}
95+
96+
[Fact]
97+
public void GetExceptionStackTraceHandlesNestedExceptions()
98+
{
99+
var method = typeof(BugReportService).GetMethod("GetExceptionStackTrace", BindingFlags.NonPublic | BindingFlags.Static);
100+
Assert.NotNull(method);
101+
102+
var deep = new FormatException("Deep");
103+
var mid = new ArgumentException("Mid", deep);
104+
var top = new InvalidOperationException("Top", mid);
105+
var result = method.Invoke(null, [top]) as string;
106+
Assert.NotNull(result);
107+
Assert.Contains("Top", result, StringComparison.Ordinal);
108+
Assert.Contains("Mid", result, StringComparison.Ordinal);
109+
Assert.Contains("Deep", result, StringComparison.Ordinal);
110+
}
111+
112+
[Fact]
113+
public void GetExceptionStackTraceLimitsDepth()
114+
{
115+
var method = typeof(BugReportService).GetMethod("GetExceptionStackTrace", BindingFlags.NonPublic | BindingFlags.Static);
116+
Assert.NotNull(method);
117+
118+
var inner = new Exception("deepest");
119+
var current = inner;
120+
for (var i = 0; i < 10; i++)
121+
{
122+
current = new Exception($"level {i}", current);
123+
}
124+
125+
var result = method.Invoke(null, [current]) as string;
126+
Assert.NotNull(result);
127+
}
128+
129+
[Fact]
130+
public void GetEnvironmentDetailsReturnsValidValues()
131+
{
132+
var method = typeof(BugReportService).GetMethod("GetEnvironmentDetails", BindingFlags.NonPublic | BindingFlags.Static);
133+
Assert.NotNull(method);
134+
135+
var result = method.Invoke(null, null);
136+
Assert.NotNull(result);
137+
138+
var type = result.GetType();
139+
var osVersion = type.GetField("Item1")?.GetValue(result) as string;
140+
var architecture = type.GetField("Item2")?.GetValue(result) as string;
141+
var processorCount = type.GetField("Item5")?.GetValue(result);
142+
143+
Assert.NotNull(osVersion);
144+
Assert.NotEmpty(osVersion);
145+
Assert.NotNull(architecture);
146+
Assert.NotEmpty(architecture);
147+
Assert.IsType<int>(processorCount);
148+
}
149+
150+
[Fact]
151+
public void AppendExceptionDetailsWithNullStackTraceDoesNotCrash()
152+
{
153+
var method = typeof(BugReportService).GetMethod("AppendExceptionDetails", BindingFlags.NonPublic | BindingFlags.Static);
154+
Assert.NotNull(method);
155+
156+
var sb = new StringBuilder();
157+
var ex = new Exception("No stack") { };
158+
var record = Record.Exception(() => method.Invoke(null, [sb, ex, 0]));
159+
Assert.Null(record);
160+
Assert.Contains("No stack", sb.ToString(), StringComparison.Ordinal);
161+
}
162+
163+
[Fact]
164+
public async Task SendBugReportAsyncReturnsFalseOnNetworkError()
165+
{
166+
var service = new BugReportService("https://invalid.example.invalid/api", TestApiKey, TestAppName);
167+
var result = await service.SendBugReportAsync("Test message");
168+
Assert.False(result);
169+
}
170+
}

0 commit comments

Comments
 (0)