Skip to content

Commit 6ee9530

Browse files
feat: refactor UpdateService for DI, add unit tests for AppHttpClient and version checking, update ReadMe and UI tooltips
1 parent 4bb7baf commit 6ee9530

19 files changed

Lines changed: 1144 additions & 198 deletions

BatchConvertToCHD.Tests/AppConfigTests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,11 @@ public void PsxPackagerExeNameIsNotEmpty()
7272
{
7373
Assert.False(string.IsNullOrEmpty(AppConfig.PsxPackagerExeName));
7474
}
75+
76+
[Fact]
77+
public void GitHubApiLatestReleaseUrlIsNotEmpty()
78+
{
79+
Assert.False(string.IsNullOrEmpty(AppConfig.GitHubApiLatestReleaseUrl));
80+
Assert.StartsWith("https://api.github.com/repos/", AppConfig.GitHubApiLatestReleaseUrl);
81+
}
7582
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System.Reflection;
2+
using System.Security.Authentication;
3+
using System.Net.Security;
4+
using BatchConvertToCHD.Services;
5+
6+
namespace BatchConvertToCHD.Tests;
7+
8+
public class AppHttpClientTests
9+
{
10+
[Fact]
11+
public void ClientReturnsNonNullHttpClient()
12+
{
13+
var client = AppHttpClient.Client;
14+
Assert.NotNull(client);
15+
}
16+
17+
[Fact]
18+
public void ClientReturnsSameInstance()
19+
{
20+
var client1 = AppHttpClient.Client;
21+
var client2 = AppHttpClient.Client;
22+
Assert.Same(client1, client2);
23+
}
24+
25+
[Fact]
26+
public void ClientHasAcceptJsonHeader()
27+
{
28+
var client = AppHttpClient.Client;
29+
Assert.True(client.DefaultRequestHeaders.Accept.Count > 0);
30+
Assert.Contains(
31+
client.DefaultRequestHeaders.Accept, static m => m.MediaType == "application/json");
32+
}
33+
34+
[Fact]
35+
public void ClientUsesTls12And13()
36+
{
37+
var handlerField = typeof(AppHttpClient).GetField(
38+
"_handler", BindingFlags.NonPublic | BindingFlags.Static);
39+
Assert.NotNull(handlerField);
40+
var handler = handlerField.GetValue(null);
41+
Assert.NotNull(handler);
42+
43+
var sslOptionsProp = handler.GetType()
44+
.GetProperty("SslOptions", BindingFlags.Public | BindingFlags.Instance);
45+
Assert.NotNull(sslOptionsProp);
46+
var sslOptions = sslOptionsProp.GetValue(handler) as SslClientAuthenticationOptions;
47+
Assert.NotNull(sslOptions);
48+
49+
Assert.True(
50+
sslOptions.EnabledSslProtocols.HasFlag(SslProtocols.Tls12),
51+
"TLS 1.2 should be enabled");
52+
Assert.True(
53+
sslOptions.EnabledSslProtocols.HasFlag(SslProtocols.Tls13),
54+
"TLS 1.3 should be enabled");
55+
}
56+
57+
[Fact]
58+
public void DisposeClearsClientAndHandler()
59+
{
60+
var clientBefore = AppHttpClient.Client;
61+
Assert.NotNull(clientBefore);
62+
63+
AppHttpClient.Dispose();
64+
65+
var clientField = typeof(AppHttpClient).GetField(
66+
"_client", BindingFlags.NonPublic | BindingFlags.Static);
67+
Assert.NotNull(clientField);
68+
Assert.Null(clientField.GetValue(null));
69+
70+
var handlerField = typeof(AppHttpClient).GetField(
71+
"_handler", BindingFlags.NonPublic | BindingFlags.Static);
72+
Assert.NotNull(handlerField);
73+
Assert.Null(handlerField.GetValue(null));
74+
}
75+
76+
[Fact]
77+
public void ClientAfterDisposeReturnsNewInstance()
78+
{
79+
var client1 = AppHttpClient.Client;
80+
AppHttpClient.Dispose();
81+
var client2 = AppHttpClient.Client;
82+
83+
Assert.NotSame(client1, client2);
84+
Assert.NotNull(client2);
85+
}
86+
87+
[Fact]
88+
public void DisposeCanBeCalledMultipleTimes()
89+
{
90+
AppHttpClient.Dispose();
91+
var exception = Record.Exception(AppHttpClient.Dispose);
92+
Assert.Null(exception);
93+
}
94+
95+
[Fact]
96+
public async Task ClientIsThreadSafe()
97+
{
98+
var clients = new HttpClient[10];
99+
var tasks = new Task[10];
100+
101+
for (var i = 0; i < 10; i++)
102+
{
103+
var index = i;
104+
tasks[i] = Task.Run(() => { clients[index] = AppHttpClient.Client; });
105+
}
106+
107+
await Task.WhenAll(tasks);
108+
109+
var first = clients[0];
110+
for (var i = 1; i < 10; i++)
111+
{
112+
Assert.Same(first, clients[i]);
113+
}
114+
}
115+
}

BatchConvertToCHD.Tests/BugReportServiceTests.cs

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -127,24 +127,14 @@ public void GetExceptionStackTraceLimitsDepth()
127127
}
128128

129129
[Fact]
130-
public void GetEnvironmentDetailsReturnsValidValues()
130+
public void GetApplicationVersionReturnsValidValue()
131131
{
132-
var method = typeof(BugReportService).GetMethod("GetEnvironmentDetails", BindingFlags.NonPublic | BindingFlags.Static);
132+
var method = typeof(BugReportService).GetMethod("GetApplicationVersion", BindingFlags.NonPublic | BindingFlags.Static);
133133
Assert.NotNull(method);
134134

135-
var result = method.Invoke(null, null);
135+
var result = method.Invoke(null, null) as string;
136136
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);
137+
Assert.NotEmpty(result);
148138
}
149139

150140
[Fact]
@@ -167,4 +157,66 @@ public async Task SendBugReportAsyncReturnsFalseOnNetworkError()
167157
var result = await service.SendBugReportAsync("Test message");
168158
Assert.False(result);
169159
}
160+
161+
[Fact]
162+
public void BuildFormattedReportExceptionWithNullFieldsDoesNotCrash()
163+
{
164+
var service = new BugReportService(TestApiUrl, TestApiKey, TestAppName);
165+
var method = typeof(BugReportService).GetMethod("BuildFormattedReport", BindingFlags.NonPublic | BindingFlags.Instance);
166+
Assert.NotNull(method);
167+
168+
var ex = Record.Exception(() =>
169+
{
170+
// Exception with no message and no stack trace
171+
var customEx = new Exception((string?)null);
172+
var result = method.Invoke(service, ["Error summary", customEx]) as string;
173+
Assert.NotNull(result);
174+
Assert.Contains("Error summary", result, StringComparison.Ordinal);
175+
});
176+
177+
Assert.Null(ex);
178+
}
179+
180+
[Fact]
181+
public void BuildFormattedReportEmptyMessageDoesNotCrash()
182+
{
183+
var service = new BugReportService(TestApiUrl, TestApiKey, TestAppName);
184+
var method = typeof(BugReportService).GetMethod("BuildFormattedReport", BindingFlags.NonPublic | BindingFlags.Instance);
185+
Assert.NotNull(method);
186+
187+
var result = method.Invoke(service, ["", null]) as string;
188+
Assert.NotNull(result);
189+
Assert.Contains("=== Error Details ===", result, StringComparison.Ordinal);
190+
}
191+
192+
[Fact]
193+
public void BuildFormattedReportExceptionWithoutStackTraceDoesNotCrash()
194+
{
195+
var service = new BugReportService(TestApiUrl, TestApiKey, TestAppName);
196+
var method = typeof(BugReportService).GetMethod("BuildFormattedReport", BindingFlags.NonPublic | BindingFlags.Instance);
197+
Assert.NotNull(method);
198+
199+
// Create exception using parameterless constructor which may not populate StackTrace immediately
200+
var customEx = new InvalidOperationException("Error with no explicit stack");
201+
var result = method.Invoke(service, ["Error with null stack", customEx]) as string;
202+
Assert.NotNull(result);
203+
Assert.Contains("Error with null stack", result, StringComparison.Ordinal);
204+
Assert.Contains("InvalidOperationException", result, StringComparison.Ordinal);
205+
}
206+
207+
[Fact]
208+
public void AppendExceptionDetailsHandlesExceptionWithoutSource()
209+
{
210+
var method = typeof(BugReportService).GetMethod("AppendExceptionDetails", BindingFlags.NonPublic | BindingFlags.Static);
211+
Assert.NotNull(method);
212+
213+
var sb = new StringBuilder();
214+
var ex = Record.Exception(() =>
215+
{
216+
method.Invoke(null, [sb, new InvalidOperationException(), 0]);
217+
});
218+
219+
Assert.Null(ex);
220+
Assert.Contains("InvalidOperationException", sb.ToString(), StringComparison.Ordinal);
221+
}
170222
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Net;
2+
3+
namespace BatchConvertToCHD.Tests;
4+
5+
public class FakeHttpMessageHandler : HttpMessageHandler
6+
{
7+
private readonly Func<HttpRequestMessage, HttpResponseMessage> _handler;
8+
9+
public FakeHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> handler)
10+
{
11+
_handler = handler;
12+
}
13+
14+
public FakeHttpMessageHandler(HttpStatusCode statusCode, string content, string contentType = "application/json")
15+
: this(_ => new HttpResponseMessage(statusCode)
16+
{
17+
Content = new StringContent(content, System.Text.Encoding.UTF8, contentType)
18+
})
19+
{
20+
}
21+
22+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
23+
{
24+
return Task.FromResult(_handler(request));
25+
}
26+
}

BatchConvertToCHD.Tests/FileItemTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ public void FileSizeSetsDisplaySize()
103103
[InlineData(1536, "1.5 KB")]
104104
[InlineData(1048576, "1 MB")]
105105
[InlineData(1073741824, "1 GB")]
106+
[InlineData(1099511627776, "1 TB")]
107+
[InlineData(1649267441664, "1.5 TB")]
108+
[InlineData(long.MaxValue, "8388608 TB")]
106109
public void DisplaySizeFormatsCorrectly(long bytes, string expected)
107110
{
108111
var item = new FileItem
@@ -113,4 +116,31 @@ public void DisplaySizeFormatsCorrectly(long bytes, string expected)
113116
item.FileSize = bytes;
114117
Assert.Equal(expected, item.DisplaySize);
115118
}
119+
120+
[Fact]
121+
public void DisplaySizeNegativeValueFormatsDirectly()
122+
{
123+
var item = new FileItem { FileSize = 1 };
124+
item.FileSize = -1;
125+
Assert.Equal("-1 B", item.DisplaySize);
126+
}
127+
128+
[Fact]
129+
public void DisplaySizeChangeFiresPropertyChanged()
130+
{
131+
var item = new FileItem();
132+
var displaySizeChanged = false;
133+
item.PropertyChanged += (_, e) =>
134+
{
135+
if (e.PropertyName == nameof(FileItem.DisplaySize))
136+
{
137+
displaySizeChanged = true;
138+
}
139+
};
140+
141+
item.FileSize = 1024;
142+
143+
Assert.True(displaySizeChanged);
144+
Assert.Equal("1 KB", item.DisplaySize);
145+
}
116146
}

BatchConvertToCHD.Tests/GameFileParserTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,4 +243,17 @@ public async Task GetReferencedFilesFromTocAsyncFileLineWithoutTypeSkips()
243243
var result = await GameFileParser.GetReferencedFilesFromTocAsync(tocPath, static _ => { }, CancellationToken.None);
244244
Assert.Empty(result);
245245
}
246+
247+
[Fact]
248+
public async Task GetReferencedFilesFromTocAsyncUnquotedFileReturnsReferencedFiles()
249+
{
250+
var tocPath = Path.Combine(_tempDir, "game.toc");
251+
const string content = "FILE track1.bin BINARY\n TRACK 01 MODE2/2352\n INDEX 01 00:00:00";
252+
await File.WriteAllTextAsync(tocPath, content);
253+
254+
var result = await GameFileParser.GetReferencedFilesFromTocAsync(tocPath, static _ => { }, CancellationToken.None);
255+
256+
Assert.Single(result);
257+
Assert.Equal(Path.Combine(_tempDir, "track1.bin"), result[0]);
258+
}
246259
}

0 commit comments

Comments
 (0)