From 5bda12ae45a7a57dd61ef733a2a662c38aec9cbf Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Mon, 2 Sep 2024 18:12:29 -0500 Subject: [PATCH] Purge cache hashes on external deletion --- Core/Net/NetFileCache.cs | 35 +- Netkan/Services/CachingHttpService.cs | 2 + Tests/Core/Cache.cs | 258 ------------- Tests/Core/Net/NetFileCacheTests.cs | 363 ++++++++++++++++++ Tests/Core/Net/NetModuleCacheTests.cs | 110 ++++++ Tests/Data/ModuleManager-2.5.1.ckan | 6 +- Tests/Data/TestData.cs | 4 +- Tests/NetKAN/Services/FileServiceTests.cs | 3 + .../DownloadAttributeTransformerTests.cs | 1 + 9 files changed, 509 insertions(+), 273 deletions(-) delete mode 100644 Tests/Core/Cache.cs create mode 100644 Tests/Core/Net/NetFileCacheTests.cs create mode 100644 Tests/Core/Net/NetModuleCacheTests.cs diff --git a/Core/Net/NetFileCache.cs b/Core/Net/NetFileCache.cs index 4ded7f8688..f7660d8f27 100644 --- a/Core/Net/NetFileCache.cs +++ b/Core/Net/NetFileCache.cs @@ -134,8 +134,15 @@ public string GetInProgressFileName(Uri url, string description) /// private void OnCacheChanged(object source, FileSystemEventArgs e) { - log.Debug("File system watcher event fired"); + log.DebugFormat("File system watcher event {0} fired for {1}", + e.ChangeType.ToString(), + e.FullPath); OnCacheChanged(); + if (e.ChangeType == WatcherChangeTypes.Deleted) + { + log.DebugFormat("Purging hashes reactively: {0}", e.FullPath); + PurgeHashes(null, e.FullPath); + } } /// @@ -231,10 +238,7 @@ public bool IsMaybeCachedZip(Uri url, DateTime? remoteTimestamp = null) // Local file too old, delete it log.Debug("Found stale file, deleting it"); File.Delete(file); - File.Delete($"{file}.sha1"); - File.Delete($"{file}.sha256"); - sha1Cache.Remove(file); - sha256Cache.Remove(file); + PurgeHashes(null, file); } } else @@ -411,7 +415,7 @@ public string Store(Uri url, TxFileManager tx_file = new TxFileManager(); - // Make sure we clear our cache entry first. + // Clear our cache entry first Remove(url); string hash = CreateURLHash(url); @@ -420,7 +424,7 @@ public string Store(Uri url, Debug.Assert( Regex.IsMatch(description, "^[A-Za-z0-9_.-]*$"), - "description isn't as filesystem safe as we thought... (#1266)"); + $"description {description} isn't as filesystem safe as we thought... (#1266)"); string fullName = string.Format("{0}-{1}", hash, Path.GetFileName(description)); string targetPath = Path.Combine(cachePath, fullName); @@ -467,13 +471,20 @@ public bool Remove(Uri url) return false; } - private void PurgeHashes(TxFileManager tx_file, string file) + private void PurgeHashes(TxFileManager? tx_file, string file) { - tx_file.Delete($"{file}.sha1"); - tx_file.Delete($"{file}.sha256"); + try + { + sha1Cache.Remove(file); + sha256Cache.Remove(file); - sha1Cache.Remove(file); - sha256Cache.Remove(file); + tx_file ??= new TxFileManager(); + tx_file.Delete($"{file}.sha1"); + tx_file.Delete($"{file}.sha256"); + } + catch + { + } } /// diff --git a/Netkan/Services/CachingHttpService.cs b/Netkan/Services/CachingHttpService.cs index 2b1a31ef96..3ad8254874 100644 --- a/Netkan/Services/CachingHttpService.cs +++ b/Netkan/Services/CachingHttpService.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.IO; + using log4net; + using CKAN.NetKAN.Model; namespace CKAN.NetKAN.Services diff --git a/Tests/Core/Cache.cs b/Tests/Core/Cache.cs deleted file mode 100644 index 9a0c8120aa..0000000000 --- a/Tests/Core/Cache.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Globalization; - -using NUnit.Framework; -using Tests.Data; - -using CKAN; - -namespace Tests.Core -{ - [TestFixture] - public class Cache - { - private string? cache_dir; - - private NetFileCache? cache; - private NetModuleCache? module_cache; - - [SetUp] - public void MakeCache() - { - cache_dir = TestData.NewTempDir(); - Directory.CreateDirectory(cache_dir); - cache = new NetFileCache(cache_dir); - module_cache = new NetModuleCache(cache_dir); - } - - [TearDown] - public void RemoveCache() - { - cache?.Dispose(); - cache = null; - module_cache?.Dispose(); - module_cache = null; - if (cache_dir != null) - { - Directory.Delete(cache_dir, true); - } - } - - [Test] - public void Sanity() - { - Assert.IsInstanceOf(cache); - Assert.IsInstanceOf(module_cache); - } - - [Test] - public void StoreRetrieve() - { - Uri url = new Uri("http://example.com/"); - string file = TestData.DogeCoinFlagZip(); - - // Our URL shouldn't be cached to begin with. - Assert.IsFalse(cache?.IsCached(url)); - - // Store our file. - cache?.Store(url, file); - - // Now it should be cached. - Assert.IsTrue(cache?.IsCached(url)); - - // Check contents match. - var cached_file = cache?.GetCachedFilename(url); - FileAssert.AreEqual(file, cached_file); - } - - [Test, TestCase("cheesy.zip","cheesy.zip"), - TestCase("Foo-1-2.3","Foo-1-2.3"), - TestCase("Foo-1-2-3","Foo-1-2-3"), - TestCase("Foo-..-etc-passwd","Foo-..-etc-passwd")] - public void NamingHints(string hint, string appendage) - { - Uri url = new Uri("http://example.com/"); - string file = TestData.DogeCoinFlagZip(); - - Assert.IsFalse(cache?.IsCached(url)); - cache?.Store(url, file, hint); - - StringAssert.EndsWith(appendage, cache?.GetCachedFilename(url)); - } - - [Test] - public void StoreRemove() - { - Uri url = new Uri("http://example.com/"); - string file = TestData.DogeCoinFlagZip(); - - Assert.IsFalse(cache?.IsCached(url)); - cache?.Store(url, file); - Assert.IsTrue(cache?.IsCached(url)); - - cache?.Remove(url); - - Assert.IsFalse(cache?.IsCached(url)); - } - - [Test] - public void CacheKraken() - { - string dir = "/this/path/better/not/exist"; - - try - { - new NetFileCache(dir); - } - catch (DirectoryNotFoundKraken kraken) - { - Assert.AreSame(dir, kraken.directory); - } - } - - [Test] - public void StoreInvalid() - { - // Try to store a nonexistent zip into a NetModuleCache - // and expect an FileNotFoundKraken - Assert.Throws(() => - module_cache?.Store( - TestData.DogeCoinFlag_101_LZMA_module, - "/DoesNotExist.zip", new Progress(percent => {}))); - - // Try to store the LZMA-format DogeCoin zip into a NetModuleCache - // and expect an InvalidModuleFileKraken - Assert.Throws(() => - module_cache?.Store( - TestData.DogeCoinFlag_101_LZMA_module, - TestData.DogeCoinFlagZipLZMA, new Progress(percent => {}))); - - // Try to store the normal DogeCoin zip into a NetModuleCache - // using the WRONG metadata (file size and hashes) - // and expect an InvalidModuleFileKraken - Assert.Throws(() => - module_cache?.Store( - TestData.DogeCoinFlag_101_LZMA_module, - TestData.DogeCoinFlagZip(), new Progress(percent => {}))); - } - - [Test] - public void DoubleCache() - { - // Store and flip files in our cache. We should always get - // the most recent file we store for any given URL. - - Uri url = new Uri("http://Double.Rainbow.What.Does.It.Mean/"); - Assert.IsFalse(cache?.IsCached(url)); - - string file1 = TestData.DogeCoinFlagZip(); - string file2 = TestData.ModuleManagerZip(); - - cache?.Store(url, file1); - FileAssert.AreEqual(file1, cache?.GetCachedFilename(url)); - - cache?.Store(url, file2); - FileAssert.AreEqual(file2, cache?.GetCachedFilename(url)); - - cache?.Store(url, file1); - FileAssert.AreEqual(file1, cache?.GetCachedFilename(url)); - } - - [Test] - public void ZipValidation() - { - // We could use any URL, but this one is awesome. <3 - Uri url = new Uri("http://kitte.nz/"); - - Assert.IsFalse(cache?.IsCached(url)); - - // Store a bad zip. - cache?.Store(url, TestData.DogeCoinFlagZipCorrupt()); - - // Make sure it's stored - Assert.IsTrue(cache?.IsCached(url)); - // Make sure it's not valid as a zip - Assert.IsFalse(NetModuleCache.ZipValid(cache?.GetCachedFilename(url) ?? "", - out _, null)); - - // Store a good zip. - cache?.Store(url, TestData.DogeCoinFlagZip()); - - // Make sure it's stored, and valid. - Assert.IsTrue(cache?.IsCached(url)); - } - - [Test] - public void ZipValid_ContainsFilenameWithBadChars_NoException() - { - // We want to inspect a localized error message below. - // Switch to English to ensure it's what we expect. - CultureInfo origUICulture = Thread.CurrentThread.CurrentUICulture; - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; - - bool valid = false; - string reason = ""; - Assert.DoesNotThrow(() => - valid = NetModuleCache.ZipValid(TestData.ZipWithBadChars, out reason, null)); - - // The file is considered valid on Linux; - // only check the reason if found invalid - if (!valid) - { - Assert.AreEqual( - @"Error in step EntryHeader for GameData/FlagPack/Flags/Weyland-Yutani from ""Alien"".png: Exception during test - 'Name is invalid'", - reason - ); - } - - // Switch back to the original locale - Thread.CurrentThread.CurrentUICulture = origUICulture; - } - - [Test] - public void ZipValid_ContainsFilenameWithUnicodeChars_Valid() - { - bool valid = false; - string? reason = null; - - Assert.DoesNotThrow(() => - valid = NetModuleCache.ZipValid(TestData.ZipWithUnicodeChars, out reason, null)); - Assert.IsTrue(valid, reason); - } - - [Test] - public void EnforceSizeLimit_UnderLimit_FileRetained() - { - // Arrange - CKAN.Registry registry = CKAN.Registry.Empty(); - long fileSize = new FileInfo(TestData.DogeCoinFlagZip()).Length; - - // Act - Uri url = new Uri("http://kitte.nz/"); - cache?.Store(url, TestData.DogeCoinFlagZip()); - cache?.EnforceSizeLimit(fileSize + 100, registry); - - // Assert - Assert.IsTrue(cache?.IsCached(url)); - } - - [Test] - public void EnforceSizeLimit_OverLimit_FileRemoved() - { - // Arrange - CKAN.Registry registry = CKAN.Registry.Empty(); - long fileSize = new FileInfo(TestData.DogeCoinFlagZip()).Length; - - // Act - Uri url = new Uri("http://kitte.nz/"); - cache?.Store(url, TestData.DogeCoinFlagZip()); - cache?.EnforceSizeLimit(fileSize - 100, registry); - - // Assert - Assert.IsFalse(cache?.IsCached(url)); - } - - } -} diff --git a/Tests/Core/Net/NetFileCacheTests.cs b/Tests/Core/Net/NetFileCacheTests.cs new file mode 100644 index 0000000000..4b1d9c35a2 --- /dev/null +++ b/Tests/Core/Net/NetFileCacheTests.cs @@ -0,0 +1,363 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; + +using NUnit.Framework; + +using CKAN; +#if NETFRAMEWORK +using CKAN.Extensions; +#endif +using Tests.Data; + +namespace Tests.Core +{ + [TestFixture] + [Category("Cache")] + public class NetFileCacheTests + { + private string? cache_dir; + private NetFileCache? cache; + + [SetUp] + public void MakeCache() + { + cache_dir = TestData.NewTempDir(); + Directory.CreateDirectory(cache_dir); + cache = new NetFileCache(cache_dir); + } + + [TearDown] + public void RemoveCache() + { + cache?.Dispose(); + cache = null; + if (cache_dir != null) + { + Directory.Delete(cache_dir, true); + } + } + + [Test] + public void Sanity() + { + Assert.IsInstanceOf(cache); + } + + [Test] + public void StoreRetrieve() + { + Uri url = new Uri("http://example.com/"); + string file = TestData.DogeCoinFlagZip(); + + // Our URL shouldn't be cached to begin with. + Assert.IsFalse(cache?.IsCached(url)); + + // Store our file. + cache?.Store(url, file); + + // Now it should be cached. + Assert.IsTrue(cache?.IsCached(url)); + + // Check contents match. + var cached_file = cache?.GetCachedFilename(url); + FileAssert.AreEqual(file, cached_file); + } + + [Test, TestCase("cheesy.zip","cheesy.zip"), + TestCase("Foo-1-2.3","Foo-1-2.3"), + TestCase("Foo-1-2-3","Foo-1-2-3"), + TestCase("Foo-..-etc-passwd","Foo-..-etc-passwd")] + public void NamingHints(string hint, string appendage) + { + Uri url = new Uri("http://example.com/"); + string file = TestData.DogeCoinFlagZip(); + + Assert.IsFalse(cache?.IsCached(url)); + cache?.Store(url, file, hint); + + StringAssert.EndsWith(appendage, cache?.GetCachedFilename(url)); + } + + [Test] + public void StoreRemove() + { + Uri url = new Uri("http://example.com/"); + string file = TestData.DogeCoinFlagZip(); + + Assert.IsFalse(cache?.IsCached(url)); + cache?.Store(url, file); + Assert.IsTrue(cache?.IsCached(url)); + + cache?.Remove(url); + + Assert.IsFalse(cache?.IsCached(url)); + } + + [Test] + public void CacheKraken() + { + string dir = "/this/path/better/not/exist"; + + try + { + new NetFileCache(dir); + } + catch (DirectoryNotFoundKraken kraken) + { + Assert.AreSame(dir, kraken.directory); + } + } + + [Test] + public void DoubleCache() + { + // Store and flip files in our cache. We should always get + // the most recent file we store for any given URL. + + Uri url = new Uri("http://Double.Rainbow.What.Does.It.Mean/"); + Assert.IsFalse(cache?.IsCached(url)); + + string file1 = TestData.DogeCoinFlagZip(); + string file2 = TestData.ModuleManagerZip(); + + cache?.Store(url, file1); + FileAssert.AreEqual(file1, cache?.GetCachedFilename(url)); + + cache?.Store(url, file2); + FileAssert.AreEqual(file2, cache?.GetCachedFilename(url)); + + cache?.Store(url, file1); + FileAssert.AreEqual(file1, cache?.GetCachedFilename(url)); + } + + [Test] + public void ZipValidation() + { + // We could use any URL, but this one is awesome. <3 + Uri url = new Uri("http://kitte.nz/"); + + Assert.IsFalse(cache?.IsCached(url)); + + // Store a bad zip. + cache?.Store(url, TestData.DogeCoinFlagZipCorrupt()); + + // Make sure it's stored + Assert.IsTrue(cache?.IsCached(url)); + + // Make sure it's not valid as a zip + Assert.IsFalse(NetModuleCache.ZipValid(cache?.GetCachedFilename(url) ?? "", + out _, null)); + + // Store a good zip. + cache?.Store(url, TestData.DogeCoinFlagZip()); + + // Make sure it's stored, and valid. + Assert.IsTrue(cache?.IsCached(url)); + } + + [Test] + public void EnforceSizeLimit_UnderLimit_FileRetained() + { + // Arrange + CKAN.Registry registry = CKAN.Registry.Empty(); + long fileSize = new FileInfo(TestData.DogeCoinFlagZip()).Length; + + // Act + Uri url = new Uri("http://kitte.nz/"); + cache?.Store(url, TestData.DogeCoinFlagZip()); + cache?.EnforceSizeLimit(fileSize + 100, registry); + + // Assert + Assert.IsTrue(cache?.IsCached(url)); + } + + [Test] + public void EnforceSizeLimit_OverLimit_FileRemoved() + { + // Arrange + CKAN.Registry registry = CKAN.Registry.Empty(); + long fileSize = new FileInfo(TestData.DogeCoinFlagZip()).Length; + + // Act + Uri url = new Uri("http://kitte.nz/"); + cache?.Store(url, TestData.DogeCoinFlagZip()); + cache?.EnforceSizeLimit(fileSize - 100, registry); + + // Assert + Assert.IsFalse(cache?.IsCached(url)); + } + + #pragma warning disable IDE0051 + private static object[] HashCachingTestCases() + => new[] + { + new object[] + { + TestData.DogeCoinFlagZip(), + TestData.DogeCoinFlag_101_module(), + } + }; + #pragma warning restore IDE0051 + + [Test, TestCaseSource("HashCachingTestCases")] + public void Store_ExternalDeletion_HashesPurged(string zipPath, + CkanModule module) + { + // Arrange + if (cache == null) + { + // Assertions don't declare non-nullability + throw new Kraken("Cache is null!"); + } + var url = module.download?.First(); + if (url == null) + { + throw new Kraken("URL is null!"); + } + var pathInCache = cache.Store(url, zipPath, + CkanModule.StandardName($"{module.identifier}-ExternalDeletionTest", + module.version)); + // Calculate the hashes to populate the hash caches + Assert.AreEqual(module.download_hash?.sha1, + cache.GetFileHashSha1(pathInCache, null), + "SHA1 hash does not match!"); + Assert.AreEqual(module.download_hash?.sha256, + cache.GetFileHashSha256(pathInCache, null), + "SHA256 hash does not match!"); + var sha1File = $"{pathInCache}.sha1"; + Assert.IsTrue(File.Exists(sha1File), + $"{sha1File} does not exist!"); + var sha256File = $"{pathInCache}.sha256"; + Assert.IsTrue(File.Exists(sha256File), + $"{sha256File} does not exist!"); + + // Act + File.Delete(pathInCache); + + // Give the asynchronous event that reacts to deletion time to happen + Thread.Sleep(100); + + // Assert + Assert.IsFalse(File.Exists(sha1File), + $"{sha1File} not deleted!"); + Assert.IsFalse(File.Exists(sha256File), + $"{sha256File} not deleted!"); + Assert.Throws(() => cache.GetFileHashSha1(pathInCache, null), + "SHA1 hash is still cached!"); + Assert.Throws(() => cache.GetFileHashSha256(pathInCache, null), + "SHA256 hash is still cached!!"); + } + + + [Test, TestCaseSource("HashCachingTestCases")] + public void GetCachedFilename_FutureTimestamp_Deleted(string zipPath, + CkanModule module) + { + // Arrange + if (cache == null) + { + // Assertions don't declare non-nullability + throw new Kraken("Cache is null!"); + } + var url = module.download?.First(); + if (url == null) + { + throw new Kraken("URL is null!"); + } + var pathInCache = cache.Store(url, zipPath, + CkanModule.StandardName($"{module.identifier}-FutureTimestampTest", + module.version)); + // Calculate the hashes to populate the hash caches + Assert.AreEqual(module.download_hash?.sha1, + cache.GetFileHashSha1(pathInCache, null), + "SHA1 hash does not match!"); + Assert.AreEqual(module.download_hash?.sha256, + cache.GetFileHashSha256(pathInCache, null), + "SHA256 hash does not match!"); + var sha1File = $"{pathInCache}.sha1"; + Assert.IsTrue(File.Exists(sha1File), + $"{sha1File} does not exist!"); + var sha256File = $"{pathInCache}.sha256"; + Assert.IsTrue(File.Exists(sha256File), + $"{sha256File} does not exist!"); + + // Act + var fileTimestamp = File.GetLastWriteTimeUtc(pathInCache); + var timelessResult = cache.GetCachedFilename(url); + var pastResult = cache.GetCachedFilename(url, fileTimestamp - TimeSpan.FromMinutes(30)); + var futureResult = cache.GetCachedFilename(url, fileTimestamp + TimeSpan.FromMinutes(30)); + + // Assert + Assert.AreEqual(pathInCache, timelessResult, + $"{pathInCache} missing with null timestamp!"); + Assert.AreEqual(pathInCache, pastResult, + $"{pathInCache} missing with past timestamp!"); + Assert.AreEqual(null, futureResult, + $"{pathInCache} not purged with future timestamp!"); + Assert.IsFalse(File.Exists(sha1File), + $"{sha1File} not deleted!"); + Assert.IsFalse(File.Exists(sha256File), + $"{sha256File} not deleted!"); + Assert.Throws(() => cache.GetFileHashSha1(pathInCache, null), + "SHA1 hash is still cached!"); + Assert.Throws(() => cache.GetFileHashSha256(pathInCache, null), + "SHA256 hash is still cached!!"); + } + + #pragma warning disable IDE0051 + private static object[] HashReplacementTestCases() + => new[] + { + new object[] + { + new string[] + { + TestData.DogeCoinFlagZipLZMA, + TestData.DogeCoinFlagZip(), + TestData.ModuleManagerZip(), + }, + new CkanModule[] + { + TestData.DogeCoinFlag_101_LZMA_module, + TestData.DogeCoinFlag_101_module(), + TestData.ModuleManagerModule() + }, + }, + }; + #pragma warning restore IDE0051 + + [Test, TestCaseSource("HashReplacementTestCases")] + public void GetCachedFilename_ReplaceZIP_HashesUpdated(string[] zipPaths, + CkanModule[] modules) + { + // Arrange + if (cache == null) + { + // Assertions don't declare non-nullability + throw new Kraken("Cache is null!"); + } + var first = modules.First(); + var url = first.download?.First(); + if (url == null) + { + throw new Kraken("URL is null!"); + } + var nameInCache = CkanModule.StandardName($"{first.identifier}-FutureTimestampTest", + first.version); + foreach ((string zipPath, CkanModule module) in zipPaths.Zip(modules)) + { + var pathInCache = cache.Store(url, zipPath, nameInCache); + Assert.AreEqual(module.download_hash?.sha1, + cache.GetFileHashSha1(pathInCache, null), + $"SHA1 hash does not match for {zipPath}!"); + Assert.AreEqual(module.download_hash?.sha256, + cache.GetFileHashSha256(pathInCache, null), + $"SHA256 hash does not match for {zipPath}!"); + } + } + + + } +} diff --git a/Tests/Core/Net/NetModuleCacheTests.cs b/Tests/Core/Net/NetModuleCacheTests.cs new file mode 100644 index 0000000000..67c8ff866f --- /dev/null +++ b/Tests/Core/Net/NetModuleCacheTests.cs @@ -0,0 +1,110 @@ +using System; +using System.IO; +using System.Threading; +using System.Globalization; + +using NUnit.Framework; + +using CKAN; +using Tests.Data; + +namespace Tests.Core +{ + [TestFixture] + [Category("Cache")] + public class NetModuleCacheTests + { + private string? cache_dir; + private NetModuleCache? module_cache; + + [SetUp] + public void MakeCache() + { + cache_dir = TestData.NewTempDir(); + Directory.CreateDirectory(cache_dir); + module_cache = new NetModuleCache(cache_dir); + } + + [TearDown] + public void RemoveCache() + { + module_cache?.Dispose(); + module_cache = null; + if (cache_dir != null) + { + Directory.Delete(cache_dir, true); + } + } + + [Test] + public void Sanity() + { + Assert.IsInstanceOf(module_cache); + } + + [Test] + public void StoreInvalid() + { + // Try to store a nonexistent zip into a NetModuleCache + // and expect an FileNotFoundKraken + Assert.Throws(() => + module_cache?.Store( + TestData.DogeCoinFlag_101_LZMA_module, + "/DoesNotExist.zip", new Progress(percent => {}))); + + // Try to store the LZMA-format DogeCoin zip into a NetModuleCache + // and expect an InvalidModuleFileKraken + Assert.Throws(() => + module_cache?.Store( + TestData.DogeCoinFlag_101_LZMA_module, + TestData.DogeCoinFlagZipLZMA, new Progress(percent => {}))); + + // Try to store the normal DogeCoin zip into a NetModuleCache + // using the WRONG metadata (file size and hashes) + // and expect an InvalidModuleFileKraken + Assert.Throws(() => + module_cache?.Store( + TestData.DogeCoinFlag_101_LZMA_module, + TestData.DogeCoinFlagZip(), new Progress(percent => {}))); + } + + [Test] + public void ZipValid_ContainsFilenameWithBadChars_NoException() + { + // We want to inspect a localized error message below. + // Switch to English to ensure it's what we expect. + CultureInfo origUICulture = Thread.CurrentThread.CurrentUICulture; + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + + bool valid = false; + string reason = ""; + Assert.DoesNotThrow(() => + valid = NetModuleCache.ZipValid(TestData.ZipWithBadChars, out reason, null)); + + // The file is considered valid on Linux; + // only check the reason if found invalid + if (!valid) + { + Assert.AreEqual( + @"Error in step EntryHeader for GameData/FlagPack/Flags/Weyland-Yutani from ""Alien"".png: Exception during test - 'Name is invalid'", + reason + ); + } + + // Switch back to the original locale + Thread.CurrentThread.CurrentUICulture = origUICulture; + } + + [Test] + public void ZipValid_ContainsFilenameWithUnicodeChars_Valid() + { + bool valid = false; + string? reason = null; + + Assert.DoesNotThrow(() => + valid = NetModuleCache.ZipValid(TestData.ZipWithUnicodeChars, out reason, null)); + Assert.IsTrue(valid, reason); + } + + } +} diff --git a/Tests/Data/ModuleManager-2.5.1.ckan b/Tests/Data/ModuleManager-2.5.1.ckan index fc1be69d07..c9412f9053 100644 --- a/Tests/Data/ModuleManager-2.5.1.ckan +++ b/Tests/Data/ModuleManager-2.5.1.ckan @@ -14,5 +14,9 @@ "file" : "ModuleManager.2.5.1.dll", "install_to" : "GameData" } - ] + ], + "download_hash": { + "sha1": "C854159B59A4BBC3415140C1E25EE014AF25A398", + "sha256": "F799628E0749ACB6B180CB4F129580AA23B7C284DCBC94043B1DA5FFA4C19D92" + } } diff --git a/Tests/Data/TestData.cs b/Tests/Data/TestData.cs index 54534e7a3a..64486c48e2 100644 --- a/Tests/Data/TestData.cs +++ b/Tests/Data/TestData.cs @@ -92,8 +92,8 @@ public static string DogeCoinFlag_101_LZMA() ""comment"": ""Generated by ks2ckan"", ""download_size"": 54178, ""download_hash"": { - ""sha1"": ""47B6ED5F502AD914744882858345BE030A29E1AA"", - ""sha256"": ""EC955DB772FBA8CAA62BF61C180D624C350D792C6F573D35A5EAEE3898DCF7C1"" + ""sha1"": ""98D4E2D1BAA246D68BF6B978C8201B7083269D0B"", + ""sha256"": ""B9460474BFA25D25431F077E8A33338C2150B8F7A7398CF8B7318992FC0039C6"" }, ""ksp_version"": ""0.25"" } diff --git a/Tests/NetKAN/Services/FileServiceTests.cs b/Tests/NetKAN/Services/FileServiceTests.cs index ffe87a43e4..eea69efa4e 100644 --- a/Tests/NetKAN/Services/FileServiceTests.cs +++ b/Tests/NetKAN/Services/FileServiceTests.cs @@ -34,6 +34,7 @@ public void TestFixtureTearDown() } [Test] + [Category("Cache")] public void GetsFileSizeCorrectly() { // Arrange @@ -49,6 +50,7 @@ public void GetsFileSizeCorrectly() } [Test] + [Category("Cache")] public void GetsFileHashSha1Correctly() { // Arrange @@ -64,6 +66,7 @@ public void GetsFileHashSha1Correctly() } [Test] + [Category("Cache")] public void GetsFileHashSha256Correctly() { // Arrange diff --git a/Tests/NetKAN/Transformers/DownloadAttributeTransformerTests.cs b/Tests/NetKAN/Transformers/DownloadAttributeTransformerTests.cs index be2c226874..1005593816 100644 --- a/Tests/NetKAN/Transformers/DownloadAttributeTransformerTests.cs +++ b/Tests/NetKAN/Transformers/DownloadAttributeTransformerTests.cs @@ -14,6 +14,7 @@ public sealed class DownloadAttributeTransformerTests private readonly TransformOptions opts = new TransformOptions(1, null, null, false, null); [Test] + [Category("Cache")] public void AddsDownloadAttributes() { // Arrange