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
29 changes: 29 additions & 0 deletions WheelWizard.Test/Features/Patches/LooseBrsarPatchFileNameTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using WheelWizard.Features.Patches;

namespace WheelWizard.Test.Features.Patches;

public class LooseBrsarPatchFileNameTests
{
[Theory]
[InlineData("[001]sound.BRWSD", "1.brwsd")]
[InlineData("[42].BRBNK", "42.brbnk")]
[InlineData("[0]name.brwsd", "0.brwsd")]
public void TaggedNamesNormalizeIdAndExtension(string name, string expected)
{
Assert.True(LooseBrsarPatchFileName.TryGetNormalizedFileName(name, out var result));
Assert.Equal(expected, result);
}

[Theory]
[InlineData("")]
[InlineData("[]sound.brwsd")]
[InlineData("[-1]sound.brwsd")]
[InlineData("[2147483648]sound.brwsd")]
[InlineData("[1]sound.szs")]
[InlineData("1.brwsd")]
public void InvalidNamesAreRejected(string name)
{
Assert.False(LooseBrsarPatchFileName.TryGetNormalizedFileName(name, out var result));
Assert.Empty(result);
}
}
47 changes: 47 additions & 0 deletions WheelWizard.Test/Features/Patches/ModPatchCompatibilityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Microsoft.Extensions.Logging.Abstractions;
using Testably.Abstractions.Testing;
using WheelWizard.Features.Patches;
using WheelWizard.Models.Mods;
using WheelWizard.Mods;

namespace WheelWizard.Test.Features.Patches;

public class ModPatchCompatibilityTests
{
[Theory]
[InlineData("course.szs", true)]
[InlineData("COURSE.SZS", true)]
[InlineData("nested/course.szs", true)]
[InlineData("revo_kart.brsar", true)]
[InlineData("[42]sound.BRWSD", true)]
[InlineData("course.tag.szs", false)]
[InlineData("notes.txt", false)]
[InlineData("sound.brwsd", false)]
public void CompatibilityUsesInjectedFilesAndRecognizesConvertibleArchives(string relativePath, bool expected)
{
var fs = new MockFileSystem();
var root = fs.Path.GetFullPath("mods/Test");
var file = fs.Path.GetFullPath(fs.Path.Combine(root, relativePath));
fs.Directory.CreateDirectory(fs.Path.GetDirectoryName(file)!);
fs.File.WriteAllText(file, "archive");
var paths = Substitute.For<IModPaths>();
paths.GetModDirectoryPath("Test").Returns(root);
var service = new ModPatchConversionService(
Substitute.For<ISzsPatchConverter>(),
NullLogger<ModPatchConversionService>.Instance,
fs,
paths,
Substitute.For<IGameBaselineStore>()
);
var mod = new Mod { Title = "Test" };

service.RefreshCompatibility(mod);

Assert.Equal(expected, mod.HasIncompatibleFiles);
Assert.Equal(expected ? new[] { file } : [], service.GetConvertibleArchiveFiles(mod));
fs.Directory.Delete(root, recursive: true);
service.RefreshCompatibility(mod);
Assert.False(mod.HasIncompatibleFiles);
Assert.Empty(service.GetConvertibleArchiveFiles(mod));
}
}
27 changes: 27 additions & 0 deletions WheelWizard.Test/Features/Recomp/RecompPlatformTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Runtime.InteropServices;
using WheelWizard.Recomp;

namespace WheelWizard.Test.Features.Recomp;

public class RecompPlatformTests
{
[Theory]
[InlineData("v1.2/../../../release", "v1-2----------release")]
[InlineData("", "")]
[InlineData("v123", "v123")]
public void CachedSetupNameKeepsTagsWithinSingleFileName(string tag, string sanitized)
{
var result = RecompPlatform.CachedSetupFileName(tag);
Assert.Equal($"WiiCompiled-Setup-{sanitized}{Path.GetExtension(RecompPlatform.SetupFileName)}", result);
Assert.Equal(result, Path.GetFileName(result));
Assert.Equal($"WiiCompiled-Setup-*{Path.GetExtension(result)}", RecompPlatform.CachedSetupSearchPattern);
}

[Theory]
[InlineData(Architecture.X64, "WiiCompiled-Setup-x86_64.AppImage")]
[InlineData(Architecture.Arm64, "WiiCompiled-Setup-aarch64.AppImage")]
[InlineData(Architecture.X86, null)]
[InlineData(Architecture.Arm, null)]
public void LinuxAssetsMatchSupportedArchitectures(Architecture architecture, string? expected) =>
Assert.Equal(expected, RecompPlatform.LinuxReleaseAssetName(architecture));
}
10 changes: 5 additions & 5 deletions WheelWizard.Test/Features/Settings/SettingsRecoveryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void DolphinEditPreservesExternalValuesAndReloadDoesNotCarryPreviousProfi
fs.File.WriteAllText("/config/GFX.ini", "[Settings]\nInternalResolution = invalid\n");
manager.ReloadSettings("/config");
Assert.Equal(1, resolution.Get());
Assert.Equal(false, fps.Get());
Assert.False(fps.Get());
Assert.Contains("invalid", fs.File.ReadAllText("/config/GFX.ini"));
}

Expand All @@ -44,10 +44,10 @@ public void RecompReloadMissingKeyUsesDefault()
manager.LoadSettings("/config/Config.toml");
fs.File.WriteAllText("/config/Config.toml", "[video]\n");
manager.ReloadSettings("/config/Config.toml");
Assert.Equal(true, setting.Get());
Assert.True(setting.Get());
fs.File.Delete("/config/Config.toml");
Assert.False(setting.Set(false));
Assert.Equal(true, setting.Get());
Assert.True(setting.Get());
}

[Fact]
Expand All @@ -66,7 +66,7 @@ public void FailedSaveRollsBackAndCanBeRetried()
var notifications = 0;
setting.Changed += _ => notifications++;
Assert.False(setting.Set(true));
Assert.Equal(false, setting.Get());
Assert.False(setting.Get());
Assert.Equal(0, notifications);
fail = false;
Assert.True(setting.Set(true));
Expand All @@ -86,7 +86,7 @@ public void JsonRetainsExistingKeysAndUnknownValuesAndPreservesCorruptFile()
var setting = new WhWzSetting<bool>("EnableAnimations", true, s => manager.SaveSettings(path, s));
manager.RegisterSetting(setting);
manager.LoadSettings(path);
Assert.Equal(false, setting.Get());
Assert.False(setting.Get());
Assert.True(setting.Set(true));
Assert.Contains("FutureSetting", fs.File.ReadAllText(path));
Assert.Equal(original, fs.File.ReadAllText(path + ".bak"));
Expand Down
47 changes: 47 additions & 0 deletions WheelWizard.Test/Shared/IO/PathSafetyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using WheelWizard.Shared.IO;

namespace WheelWizard.Test.Shared.IO;

public class PathSafetyTests
{
[Theory]
[InlineData("../outside.bin")]
[InlineData("nested/../../outside.bin")]
[InlineData("nested\\..\\outside.bin")]
[InlineData("/absolute.bin")]
[InlineData("C:\\absolute.bin")]
[InlineData("C:/absolute.bin")]
[InlineData(" ")]
public void UnsafeRelativePathsAreRejected(string path)
{
Assert.False(PathSafety.TryGetPathWithinDirectory(Path.GetTempPath(), path, out var fullPath));
Assert.Empty(fullPath);
}

[Fact]
public void NestedPathIsNormalizedInsideDirectory()
{
var root = Path.Combine(Path.GetTempPath(), "PathSafetyTests");
Assert.True(PathSafety.TryGetPathWithinDirectory(root, " ./nested\\file.bin ", out var path));
Assert.Equal(Path.Combine(root, "nested", "file.bin"), path);
Assert.False(PathSafety.IsPathWithinDirectory(root, root + "-sibling/file.bin"));
Assert.True(PathSafety.IsPathWithinDirectory(root, root));
}

[Fact]
public void ColonInSegmentFollowsPlatformRules()
{
var accepted = PathSafety.TryGetPathWithinDirectory(Path.GetTempPath(), "nested/data:variant.bin", out _);
Assert.Equal(!OperatingSystem.IsWindows(), accepted);
}

[Theory]
[InlineData("folder\\mii.bin", "mii.bin")]
[InlineData("folder/mii.bin", "mii.bin")]
[InlineData("\"mii.bin\"", "mii.bin")]
public void SafeFileNameReturnsOnlyLeaf(string input, string expected)
{
Assert.True(PathSafety.TryGetSafeFileName(input, out var result));
Assert.Equal(expected, result);
}
}
11 changes: 10 additions & 1 deletion WheelWizard.UI.Test/ModInstallOwnerLifetimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Avalonia.Interactivity;
using Avalonia.Threading;
using NSubstitute;
using Testably.Abstractions;
using WheelWizard.GameBanana;
using WheelWizard.Mods;
using WheelWizard.Mods.Views.Dialogs;
Expand Down Expand Up @@ -47,7 +48,15 @@ public async Task ClosedOwner_CancelsDownloadAndSuppressesLateSuccessOrFailureDi
// Deliberately return late instead of honoring cancellation, to exercise the race.
return finished.Task;
});
var content = new ModContent(manager, gameBanana, downloads, Substitute.For<IGameBananaMediaService>(), paths, presentation);
var content = new ModContent(
new RealFileSystem(),
manager,
gameBanana,
downloads,
Substitute.For<IGameBananaMediaService>(),
paths,
presentation
);
var mod = ModTestFixture.CreateMod(123, "Test mod");
mod.Files = [new() { FileName = "mod.zip", DownloadUrl = "https://example.com/mod.zip" }];
typeof(ModContent).GetProperty("CurrentMod", BindingFlags.NonPublic | BindingFlags.Instance)!.SetValue(content, mod);
Expand Down
2 changes: 2 additions & 0 deletions WheelWizard.UI.Test/ModTestFixture.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using NSubstitute;
using Testably.Abstractions.Testing;
using WheelWizard.GameBanana;
using WheelWizard.GameBanana.Domain;
using WheelWizard.Mods;
Expand All @@ -11,6 +12,7 @@ internal static class ModTestFixture
{
public static ModContent CreateContent(IGameBananaSingletonService api, IGameBananaMediaService? media = null) =>
new(
new MockFileSystem(),
Substitute.For<IModManager>(),
api,
Substitute.For<IDownloadService>(),
Expand Down
30 changes: 16 additions & 14 deletions WheelWizard.UI.Test/SharedControlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,11 @@ public void Dropdown_SearchFiltersWithoutChangingSelectionAndShowsEmptyStates()
Dispatcher.UIThread.RunJobs();
window.UpdateLayout();
var popup = dropdown.GetVisualDescendants().OfType<Avalonia.Controls.Primitives.Popup>().Single();
var search = popup.Child!.GetVisualDescendants().OfType<InputField>().Single();
var empty = popup.Child.GetVisualDescendants().OfType<TextBlock>().Single(x => x.Name == "PART_Empty");
var width = popup.Child.Bounds.Width;
var height = popup.Child.Bounds.Height;
var popupChild = popup.Child!;
var search = popupChild.GetVisualDescendants().OfType<InputField>().Single();
var empty = popupChild.GetVisualDescendants().OfType<TextBlock>().Single(x => x.Name == "PART_Empty");
var width = popupChild.Bounds.Width;
var height = popupChild.Bounds.Height;
Assert.True(width > dropdown.Bounds.Width);
Dispatcher.UIThread.RunJobs();
Assert.True(search.IsFocused);
Expand All @@ -484,26 +485,26 @@ public void Dropdown_SearchFiltersWithoutChangingSelectionAndShowsEmptyStates()
Assert.True(dropdown.ContainerFromIndex(0)!.IsVisible);
Assert.False(dropdown.ContainerFromIndex(1)!.IsVisible);
Assert.Equal(1, dropdown.SelectedIndex);
Assert.Equal(width, popup.Child.Bounds.Width);
Assert.True(popup.Child.Bounds.Height < height);
Assert.Equal(width, popupChild.Bounds.Width);
Assert.True(popupChild.Bounds.Height < height);
search.Text = "English";
Dispatcher.UIThread.RunJobs();
window.UpdateLayout();
Assert.Equal(width, popup.Child.Bounds.Width);
Assert.True(popup.Child.Bounds.Height < height);
Assert.Equal(width, popupChild.Bounds.Width);
Assert.True(popupChild.Bounds.Height < height);
Assert.True(search.IsFocused);
Assert.True(popup.IsOpen);
search.Text = "not a language";
Dispatcher.UIThread.RunJobs();
window.UpdateLayout();
Assert.True(empty.IsVisible);
Assert.Equal(width, popup.Child.Bounds.Width);
Assert.True(popup.Child.Bounds.Height < height);
Assert.Equal(width, popupChild.Bounds.Width);
Assert.True(popupChild.Bounds.Height < height);
Assert.Equal("No options found", empty.Text);
search.Text = "";
Dispatcher.UIThread.RunJobs();
window.UpdateLayout();
Assert.Equal(height, popup.Child.Bounds.Height);
Assert.Equal(height, popupChild.Bounds.Height);
Assert.False(empty.IsVisible);
Assert.True(search.IsFocused, "Search lost focus after restoring options.");
search.Text = "disabled";
Expand Down Expand Up @@ -691,7 +692,8 @@ public void Dropdown_OptionsCanMixIconsAndPlainText()
window.UpdateLayout();
Assert.Same(iconOption, dropdown.SelectedItem);
var fieldIcon = Assert.Single(
dropdown.GetVisualDescendants().OfType<PathIcon>().Where(icon => icon.Classes.Contains("DropdownOptionIcon"))
dropdown.GetVisualDescendants().OfType<PathIcon>(),
icon => icon.Classes.Contains("DropdownOptionIcon")
);
Assert.Same(iconOption.Icon, fieldIcon.Data);
Assert.Equal(
Expand All @@ -701,7 +703,7 @@ public void Dropdown_OptionsCanMixIconsAndPlainText()
dropdown.IsDropDownOpen = true;
window.UpdateLayout();
var popup = dropdown.GetVisualDescendants().OfType<Avalonia.Controls.Primitives.Popup>().Single();
Assert.Single(popup.Child!.GetVisualDescendants().OfType<PathIcon>().Where(icon => icon.Classes.Contains("DropdownOptionIcon")));
Assert.Single(popup.Child!.GetVisualDescendants().OfType<PathIcon>(), icon => icon.Classes.Contains("DropdownOptionIcon"));
Assert.Equal(
(Color)window.FindResource("Brand300")!,
Assert.IsAssignableFrom<ISolidColorBrush>(Assert.IsType<DropdownItem>(dropdown.ContainerFromIndex(0)).Foreground).Color
Expand All @@ -710,7 +712,7 @@ public void Dropdown_OptionsCanMixIconsAndPlainText()
dropdown.SelectedIndex = 1;
window.UpdateLayout();
Assert.Same(plainOption, dropdown.SelectedItem);
Assert.Empty(dropdown.GetVisualDescendants().OfType<PathIcon>().Where(icon => icon.Classes.Contains("DropdownOptionIcon")));
Assert.DoesNotContain(dropdown.GetVisualDescendants().OfType<PathIcon>(), icon => icon.Classes.Contains("DropdownOptionIcon"));
Assert.Contains(dropdown.GetVisualDescendants().OfType<TextBlock>(), text => text.Text == plainOption.Text);
}

Expand Down
23 changes: 15 additions & 8 deletions WheelWizard/Features/CustomDistributions/RetroRewindBeta.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,9 @@ private static bool IsBadPasswordException(Exception ex)
private bool TryGetRelativeExtractionPath(string normalizedPath, out string relativePath)
{
relativePath = string.Empty;
var archivePath = normalizedPath.Replace(Path.DirectorySeparatorChar, '/').Replace(Path.AltDirectorySeparatorChar, '/');
var archivePath = normalizedPath
.Replace(_fileSystem.Path.DirectorySeparatorChar, '/')
.Replace(_fileSystem.Path.AltDirectorySeparatorChar, '/');

if (archivePath.Equals(FolderName, StringComparison.OrdinalIgnoreCase))
{
Expand All @@ -271,7 +273,10 @@ private bool TryGetRelativeExtractionPath(string normalizedPath, out string rela

if (archivePath.StartsWith($"{FolderName}/", StringComparison.OrdinalIgnoreCase))
{
relativePath = Path.Combine(FolderName, archivePath.Substring(FolderName.Length + 1).Replace('/', Path.DirectorySeparatorChar));
relativePath = _fileSystem.Path.Combine(
FolderName,
archivePath.Substring(FolderName.Length + 1).Replace('/', _fileSystem.Path.DirectorySeparatorChar)
);
return true;
}

Expand All @@ -283,9 +288,9 @@ private bool TryGetRelativeExtractionPath(string normalizedPath, out string rela

if (archivePath.StartsWith($"{XMLFolderName}/", StringComparison.OrdinalIgnoreCase))
{
relativePath = Path.Combine(
relativePath = _fileSystem.Path.Combine(
XMLFolderName,
archivePath.Substring(XMLFolderName.Length + 1).Replace('/', Path.DirectorySeparatorChar)
archivePath.Substring(XMLFolderName.Length + 1).Replace('/', _fileSystem.Path.DirectorySeparatorChar)
);
return true;
}
Expand Down Expand Up @@ -331,16 +336,18 @@ private OperationResult<List<string>> MoveExtractedFiles(string tempExtractionPa
return Ok(manifestEntries);
}

private static bool IsRiivolutionPath(string relativePath)
private bool IsRiivolutionPath(string relativePath)
{
var normalized = relativePath.Replace(Path.DirectorySeparatorChar, '/').Replace(Path.AltDirectorySeparatorChar, '/');
var normalized = relativePath
.Replace(_fileSystem.Path.DirectorySeparatorChar, '/')
.Replace(_fileSystem.Path.AltDirectorySeparatorChar, '/');
return normalized.StartsWith("riivolution/", StringComparison.OrdinalIgnoreCase)
|| normalized.Equals("riivolution", StringComparison.OrdinalIgnoreCase);
}

private static bool IsBetaRiivolutionFile(string relativePath)
private bool IsBetaRiivolutionFile(string relativePath)
{
var fileName = Path.GetFileName(relativePath);
var fileName = _fileSystem.Path.GetFileName(relativePath);
return fileName.StartsWith("RRBeta", StringComparison.OrdinalIgnoreCase);
}

Expand Down
4 changes: 2 additions & 2 deletions WheelWizard/Features/Launching/RetroRewindLaunchDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ private static List<OptionConfig> BuildOptions(string sectionName, int myStuffCh
return options;
}

private static (string SectionName, int MyStuffChoice, bool EnableSeparateSave) GetLaunchInfo(string xmlFilePath)
private (string SectionName, int MyStuffChoice, bool EnableSeparateSave) GetLaunchInfo(string xmlFilePath)
{
var fileName = Path.GetFileName(xmlFilePath);
var fileName = fileSystem.Path.GetFileName(xmlFilePath);
if (fileName.Equals("RRBeta.xml", StringComparison.OrdinalIgnoreCase))
return ("Retro Rewind Beta", 0, true);

Expand Down
Loading
Loading