Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better caching #18

Merged
merged 8 commits into from
Dec 3, 2024
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
1 change: 0 additions & 1 deletion SimpleCDN.Tests/ByteCountFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using SimpleCDN.Helpers;
using System.Net.Sockets;

namespace SimpleCDN.Tests
{
Expand Down
57 changes: 19 additions & 38 deletions SimpleCDN.Tests/CDNLoaderTests.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
using SimpleCDN.Configuration;
using SimpleCDN.Helpers;
using SimpleCDN.Services;
using SimpleCDN.Tests.Mocks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SimpleCDN.Tests
{
Expand All @@ -16,42 +11,32 @@ public class CDNLoaderTests
const string JSON_FILENAME = "file.json";
const string TEXT_FILENAME = "data/file.txt";
const string SVG_FILENAME = "data/image.svg";
const string DEEPLY_NESED_FILENAME = "data/nested/deeply/nested/file.txt";
const string INACCESSIBLE_FILENAME = "../inaccesible.txt";

const string JSON_CONTENT = "{}";
const string TEXT_CONTENT = "Hello, world!";
const string SVG_CONTENT = "<svg></svg>";


private static string BaseFolder => Path.Combine(Path.GetTempPath(), "SimpleCDN.Tests");
private static string TempFolder => Path.Combine(BaseFolder, "wwwroot");
private static string InaccesibleFile => Path.Combine(BaseFolder, "inaccesible.txt");
private static string JSONFile => Path.Combine(TempFolder, JSON_FILENAME);
private static string TextFile => Path.Combine(TempFolder, TEXT_FILENAME);
private static string SVGFile => Path.Combine(TempFolder, SVG_FILENAME);

private static CDNLoader CreateLoader()
private static readonly Dictionary<string, MockFile> Files = new()
{
var options = new OptionsMock<CDNConfiguration>(new() { DataRoot = TempFolder });

return new CDNLoader(new MockWebHostEnvironment(), options, new IndexGenerator(options), new MockLogger<CDNLoader>());
}
["/" + JSON_FILENAME] = new(DateTime.Now, Encoding.UTF8.GetBytes(JSON_CONTENT)),
["/" + TEXT_FILENAME] = new(DateTime.Now, Encoding.UTF8.GetBytes(TEXT_CONTENT)),
["/" + SVG_FILENAME] = new(DateTime.Now, Encoding.UTF8.GetBytes(SVG_CONTENT)),
["/" + DEEPLY_NESED_FILENAME] = new(DateTime.Now, Encoding.UTF8.GetBytes(TEXT_CONTENT)),
["/" + INACCESSIBLE_FILENAME] = new(DateTime.Now, Encoding.UTF8.GetBytes(":(")),
};

[SetUp]
public void Setup()
private static CDNLoader CreateLoader()
{
// Clean up previous test data if it exists
// This could happen if the previous test was interrupted
if (Directory.Exists(BaseFolder))
{
Directory.Delete(BaseFolder, true);
}
var options = new OptionsMock<CDNConfiguration>(new() { DataRoot = "/" });

Directory.CreateDirectory(TempFolder);
File.WriteAllText(Path.Combine(BaseFolder, InaccesibleFile), ":(");
File.WriteAllText(JSONFile, JSON_CONTENT);
Directory.CreateDirectory(Path.Combine(TempFolder, "data"));
File.WriteAllText(TextFile, TEXT_CONTENT);
File.WriteAllText(SVGFile, SVG_CONTENT);
return new CDNLoader(
new MockWebHostEnvironment(),
options,
new IndexGenerator(options, new MockLogger<IndexGenerator>()),
new MockCacheManager(),
new MockPhysicalFileReader(Files));
}

[TestCase("../inaccesible.txt", TestName = "File in parent directory")]
Expand Down Expand Up @@ -82,6 +67,8 @@ public void Test_NonExistentFile_IsInaccesible(string name)
[TestCase("/" + JSON_FILENAME, JSON_CONTENT, "application/json", TestName = "Existing JSON File relative to root")]
[TestCase("/" + TEXT_FILENAME, TEXT_CONTENT, "text/plain", TestName = "Existing Plain Text File relative to root")]
[TestCase("/" + SVG_FILENAME, SVG_CONTENT, "image/svg+xml", TestName = "Existing SVG File relative to root")]
[TestCase("/../" + SVG_FILENAME, SVG_CONTENT, "image/svg+xml", TestName = "Existing SVG File with path traversal at root")]
[TestCase("/non-existent/../" + SVG_FILENAME, SVG_CONTENT, "image/svg+xml", TestName = "Existing SVG File with path traversal")]
public void Test_AccessibleFiles(string name, string content, string mediaType)
{
var loader = CreateLoader();
Expand All @@ -94,11 +81,5 @@ public void Test_AccessibleFiles(string name, string content, string mediaType)
Assert.That(file.MediaType, Is.EqualTo(mediaType));
});
}

[TearDown]
public void TearDown()
{
Directory.Delete(BaseFolder, true);
}
}
}
74 changes: 74 additions & 0 deletions SimpleCDN.Tests/CacheManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using SimpleCDN.Cache;
using SimpleCDN.Services;
using SimpleCDN.Tests.Mocks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SimpleCDN.Tests
{
[TestFixture(TestName = "Cache Manager Tests")]
public class CacheManagerTests
{
const string TEST_DATA_1 = "Hello, World!";
const string TEST_DATA_2 = TEST_DATA_1 + TEST_DATA_1 + TEST_DATA_1 + TEST_DATA_1 + TEST_DATA_1 + TEST_DATA_1;
const string TEST_DATA_3 = TEST_DATA_2 + TEST_DATA_2 + TEST_DATA_2 + TEST_DATA_2 + TEST_DATA_2 + TEST_DATA_2;
const string TEST_PATH = "/hello.txt";

[TestCase(TEST_DATA_1, TestName = "Cache Manager Add small data")]
[TestCase(TEST_DATA_2, TestName = "Cache Manager Add medium data")]
[TestCase(TEST_DATA_3, TestName = "Cache Manager Add big data")]
public void Test_Add_Exists(string data)
{
var cacheImplementation = new DistributedCacheMock();

var cache = new CacheManager(cacheImplementation);

var file = new CachedFile
{
Content = Encoding.UTF8.GetBytes(data),
Compression = CompressionAlgorithm.None,
MimeType = MimeType.Text,
LastModified = DateTimeOffset.Now,
Size = 0 // can be anything as the content is not compressed
};

cache.CacheFile(TEST_PATH, file);

Assert.That(cacheImplementation.Values, Has.Count.EqualTo(1));
Assert.Multiple(() =>
{
Assert.That(cacheImplementation.Values.Single().Key, Is.EqualTo(TEST_PATH));
Assert.That(cacheImplementation.Values.Single().Value, Is.EqualTo(file.GetBytes()));
});
}

[TestCase(TEST_DATA_1, TestName = "Cache Manager Add Remove small data")]
[TestCase(TEST_DATA_2, TestName = "Cache Manager Add Remove medium data")]
[TestCase(TEST_DATA_3, TestName = "Cache Manager Add Remove big data")]
public void Test_Add_Remove_DoesNotExist(string data)
{
var cacheImplementation = new DistributedCacheMock();
var cache = new CacheManager(cacheImplementation);

var file = new CachedFile
{
Content = Encoding.UTF8.GetBytes(data),
Compression = CompressionAlgorithm.None,
MimeType = MimeType.Text,
LastModified = DateTimeOffset.Now,
Size = 0 // can be anything as the content is not compressed
};

cache.CacheFile(TEST_PATH, file);

Assert.That(cacheImplementation.Values, Has.Count.EqualTo(1));

cache.TryRemove(TEST_PATH);

Assert.That(cacheImplementation.Values, Has.Count.EqualTo(0));
}
}
}
36 changes: 36 additions & 0 deletions SimpleCDN.Tests/CachedFileBinarizationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using SimpleCDN.Cache;
using System.Text;

namespace SimpleCDN.Tests
{
[TestFixture(TestName = "Cached File Binarization Tests")]
public class CachedFileBinarizationTests
{
[TestCase(TestName = "Convert To-From")]
public void ConvertToFrom()
{
var file = new CachedFile
{
Content = Encoding.UTF8.GetBytes("Hello, World!"),
Compression = CompressionAlgorithm.None,
MimeType = MimeType.Text,
LastModified = DateTimeOffset.Now,
Size = 0 // can be anything as the content is not compressed
};

var bytes = file.GetBytes();
var newFile = CachedFile.FromBytes(bytes);

Assert.That(newFile, Is.Not.Null);

Assert.Multiple(() =>
{
Assert.That(newFile!.Content, Is.EqualTo(file.Content));
Assert.That(newFile.Compression, Is.EqualTo(file.Compression));
Assert.That(newFile.MimeType, Is.EqualTo(file.MimeType));
Assert.That(newFile.LastModified, Is.EqualTo(file.LastModified));
Assert.That(newFile.Size, Is.EqualTo(file.Size));
});
}
}
}
4 changes: 2 additions & 2 deletions SimpleCDN.Tests/GZipTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class GZipTests
[TestCase(HTML_CONTENT, TestName = "HTML Compression and Decompression")]
public void Compress_Decompress(string inputText)
{
byte[] data = Encoding.UTF8.GetBytes(inputText);
var data = Encoding.UTF8.GetBytes(inputText);
var dataSpan = data.AsSpan();

var compressed = GZipHelpers.TryCompress(ref dataSpan);
Expand All @@ -26,7 +26,7 @@ public void Compress_Decompress(string inputText)
[TestCase("{}", TestName = "Tiny JSON does not compress")]
public void Compress_SmallData_Fails(string inputText)
{
byte[] data = Encoding.UTF8.GetBytes(inputText);
var data = Encoding.UTF8.GetBytes(inputText);
var dataSpan = data.AsSpan();
var compressed = GZipHelpers.TryCompress(ref dataSpan);
Assert.That(compressed, Is.False);
Expand Down
60 changes: 60 additions & 0 deletions SimpleCDN.Tests/Mocks/DistributedCacheMock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Microsoft.Extensions.Caching.Distributed;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SimpleCDN.Tests.Mocks
{
internal class DistributedCacheMock : IDistributedCache
{
public Dictionary<string, byte[]> Values { get; set; } = [];

public byte[]? Get(string key) => Values[key];
public Task<byte[]?> GetAsync(string key, CancellationToken token = default)
{
if (token.IsCancellationRequested) return Task.FromCanceled<byte[]?>(token);

if (Values.TryGetValue(key, out var value))
{
return Task.FromResult<byte[]?>(value);
}

return Task.FromResult<byte[]?>(null);
}

public void Refresh(string key) { }

public Task RefreshAsync(string key, CancellationToken token = default)
{
if (token.IsCancellationRequested) return Task.FromCanceled(token);

Refresh(key);

return Task.CompletedTask;
}

public void Remove(string key) => Values.Remove(key);
public Task RemoveAsync(string key, CancellationToken token = default)
{
if (token.IsCancellationRequested) return Task.FromCanceled(token);

Remove(key);

return Task.CompletedTask;
}

public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
{
Values[key] = value;
}

public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
{
if (token.IsCancellationRequested) return Task.FromCanceled(token);
Set(key, value, options);
return Task.CompletedTask;
}
}
}
23 changes: 23 additions & 0 deletions SimpleCDN.Tests/Mocks/MockCacheManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using SimpleCDN.Cache;
using SimpleCDN.Services;
using System.Diagnostics.CodeAnalysis;

namespace SimpleCDN.Tests.Mocks
{
internal class MockCacheManager : ICacheManager
{
public void CacheFile(string path, byte[] content, int realSize, DateTimeOffset lastModified, MimeType mimeType, CompressionAlgorithm compression) { }
public void CacheFile(string path, CachedFile file) { }
public bool TryGetValue(string key, [NotNullWhen(true)] out CachedFile? value)
{
value = null;
return false;
}
public bool TryRemove(string key) => true;
public bool TryRemove(string key, [NotNullWhen(true)] out CachedFile? value)
{
value = null;
return false;
}
}
}
5 changes: 0 additions & 5 deletions SimpleCDN.Tests/Mocks/MockLogger.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SimpleCDN.Tests.Mocks
{
Expand Down
4 changes: 2 additions & 2 deletions SimpleCDN.Tests/Mocks/MockOptionsMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ namespace SimpleCDN.Tests.Mocks
internal class OptionsMock<T>(T value) : IOptionsMonitor<T>, IOptions<T>, IOptionsSnapshot<T> where T : class
{
public T CurrentValue => value;
public T Value => value;
public T Value => CurrentValue;

public T Get(string? name) => value;
public T Get(string? name) => CurrentValue;
public IDisposable? OnChange(Action<T, string?> listener) => new MockDisposable();
}
}
Loading
Loading