Skip to content
Open
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
3 changes: 2 additions & 1 deletion FileOrganizer.Tests/FileOrganizer.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<TargetFramework>net10.0-windows</TargetFramework>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
Expand Down
191 changes: 191 additions & 0 deletions FileOrganizer.Tests/MetadataEngineTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
using System;
using System.IO;
using FileOrganizer.Engines;
using Xunit;

namespace FileOrganizer.Tests
{
public class MetadataEngineTests : IDisposable
{
private readonly string _tempDirectory;
private readonly MetadataEngine _metadataEngine;

public MetadataEngineTests()
{
_tempDirectory = Path.Combine(Path.GetTempPath(), "FileOrganizer_MetadataTests_" + Guid.NewGuid());
Directory.CreateDirectory(_tempDirectory);
_metadataEngine = new MetadataEngine();
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void GetSubFolder_InvalidFilePath_ReturnsEmptyString(string? filePath)
{
var result = _metadataEngine.GetSubFolder(filePath);
Assert.Equal("", result);
}

[Fact]
public void GetSubFolder_NonExistentFile_ReturnsEmptyString()
{
string path = Path.Combine(_tempDirectory, "doesnotexist.jpg");
var result = _metadataEngine.GetSubFolder(path);
Assert.Equal("", result);
}

[Fact]
public void GetSubFolder_UnhandledExtension_ReturnsEmptyString()
{
string path = Path.Combine(_tempDirectory, "test.txt");
File.WriteAllText(path, "dummy text content");
var result = _metadataEngine.GetSubFolder(path);
Assert.Equal("", result);
}

[Fact]
public void GetSubFolder_EmptyAudioFile_ReturnsUnknownArtist()
{
// TagLib# will throw CorruptFileException for empty/invalid mp3
string path = Path.Combine(_tempDirectory, "empty.mp3");
File.WriteAllBytes(path, Array.Empty<byte>());
var result = _metadataEngine.GetSubFolder(path);
Assert.Equal("Unknown Artist", result);
}

[Fact]
public void GetSubFolder_InvalidImageFile_ReturnsUnsortedPhotos()
{
// MetadataExtractor will fail to read dimensions/EXIF
string path = Path.Combine(_tempDirectory, "invalid.jpg");
File.WriteAllText(path, "not a real image");
var result = _metadataEngine.GetSubFolder(path);
Assert.Equal("Unsorted Photos", result);
}

[Fact]
public void GetSubFolder_SmallPngImage_ReturnsIconsAndGraphics()
{
string path = Path.Combine(_tempDirectory, "icon.png");

// Minimal 1x1 valid PNG byte array
byte[] png1x1 = new byte[] {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // width 1, height 1
0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89,
0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54,
0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01,
0x0d, 0x0a, 0x2d, 0xb4, 0x00, 0x00, 0x00, 0x00,
0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82
};

File.WriteAllBytes(path, png1x1);
var result = _metadataEngine.GetSubFolder(path);
Assert.Equal("Icons & Graphics", result);
}

[Fact]
public void GetSubFolder_LargePngImage_ReturnsUnsortedPhotos()
{
string path = Path.Combine(_tempDirectory, "large.png");

// Minimal 512x512 valid PNG byte array
byte[] png512x512 = new byte[] {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, // width 512, height 512
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x2b, 0xba, 0x25,
0x00, 0x00, 0x00, 0x0b, 0x49, 0x44, 0x41, 0x54,
0x08, 0xd7, 0x63, 0xf8, 0xff, 0xff, 0x3f, 0x00, 0x05, 0xfe, 0x02, 0xfe, 0xa3, 0xe1, 0x24, 0x76,
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82
};

File.WriteAllBytes(path, png512x512);
var result = _metadataEngine.GetSubFolder(path);
Assert.Equal("Unsorted Photos", result);
}

private string CreateMockMp3WithArtist(string filename, string artistName)
{
string path = Path.Combine(_tempDirectory, filename);

// Create a minimal valid MP3 frame so TagLib doesn't throw CorruptFileException
byte[] mp3Frame = new byte[] { 0xFF, 0xFB, 0x90, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
byte[] fileBytes = new byte[1024];
Array.Copy(mp3Frame, fileBytes, mp3Frame.Length);

File.WriteAllBytes(path, fileBytes);

using (var tfile = TagLib.File.Create(path))
{
if (artistName != null)
{
tfile.Tag.Performers = new[] { artistName };
}
tfile.Save();
}

return path;
}

[Theory]
[InlineData("AC/DC", "ACDC")]
[InlineData("The:Who", "TheWho")]
[InlineData("Motley*Crue", "MotleyCrue")]
[InlineData("Artist?Name", "ArtistName")]
[InlineData("Artist\"Name\"", "ArtistName")]
[InlineData("Artist<Name>", "ArtistName")]
[InlineData("Artist|Name", "ArtistName")]
[InlineData(" Valid Name ", "Valid Name")]
public void GetSubFolder_AudioWithInvalidPathChars_SanitizesArtistName(string artistName, string expectedSubFolder)
{
string path = CreateMockMp3WithArtist("test.mp3", artistName);
var result = _metadataEngine.GetSubFolder(path);

// On non-Windows platforms like Linux, Path.GetInvalidFileNameChars() only contains \0 and /
// So for cross-platform test accuracy, we check if the char is actually invalid on the current host before asserting.
// If the char isn't considered invalid on the host OS, it won't be sanitized.
string expectedHostSpecific = expectedSubFolder;
if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
{
expectedHostSpecific = artistName.Trim();
foreach (char c in Path.GetInvalidFileNameChars())
{
expectedHostSpecific = expectedHostSpecific.Replace(c.ToString(), "");
}

// TagLib splits Performers on '/', so "AC/DC" becomes an array ["AC", "DC"].
// FirstPerformer will just return "AC".
if (artistName == "AC/DC")
{
expectedHostSpecific = "AC";
}
}

Assert.Equal(expectedHostSpecific, result);
}

[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData("///")]
public void GetSubFolder_AudioWithOnlyInvalidCharsOrEmpty_ReturnsUnknown(string artistName)
{
string path = CreateMockMp3WithArtist("empty.mp3", artistName);
var result = _metadataEngine.GetSubFolder(path);

string expected = string.IsNullOrWhiteSpace(artistName) ? "Unknown Artist" : "Unknown";
Assert.Equal(expected, result);
}

public void Dispose()
{
if (Directory.Exists(_tempDirectory))
{
Directory.Delete(_tempDirectory, true);
}
}
}
}
1 change: 1 addition & 0 deletions FileOrganizer.Tests/RoutingEngineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public void Route_ShouldMapExecutablesCorrectly(string filename, string expected
[InlineData("config.json", "Other")]
[InlineData("unknown.xyz", "Other")]
[InlineData("noextension", "Other")]
[InlineData("file_with_no_ext", "Other")]
public void Route_ShouldMapOtherCorrectly(string filename, string expectedFolder)
{
var result = _routingEngine.GetTargetFolder(filename);
Expand Down
3 changes: 2 additions & 1 deletion FileOrganizer/FileOrganizer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<UseWPF>true</UseWPF>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>

<ItemGroup>
Expand Down
3 changes: 0 additions & 3 deletions FileOrganizer/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,6 @@ private async void ExecuteButton_Click(object sender, RoutedEventArgs e)
ExecuteButton.IsEnabled = true;
AnalyzeButton.IsEnabled = true;
}
finally
{
}
}

private void ClearPlan()
Expand Down
11 changes: 0 additions & 11 deletions build_output.txt

This file was deleted.

15 changes: 0 additions & 15 deletions build_output2.txt

This file was deleted.