diff --git a/DeepDrftContent/Controllers/TrackController.cs b/DeepDrftContent/Controllers/TrackController.cs new file mode 100644 index 0000000..1511d47 --- /dev/null +++ b/DeepDrftContent/Controllers/TrackController.cs @@ -0,0 +1,27 @@ +using DeepDrftContent.FileDatabase.Models; +using DeepDrftContent.FileDatabase.Services; +using Microsoft.AspNetCore.Mvc; + +namespace DeepDrftContent.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class TrackController : ControllerBase +{ + private readonly EntryKey _vaultKey = new("tracks", MediaVaultType.Audio); + private readonly FileDatabase.Services.FileDatabase _fileDatabase; + + public TrackController(FileDatabase.Services.FileDatabase fileDatabase) + { + _fileDatabase = fileDatabase; + } + + [HttpGet("{trackId}")] + public async Task> GetTrack([FromQuery] string trackId) + { + if (_fileDatabase.GetVault(_vaultKey) is not AudioVault vault) { return NotFound(); } + var file = await vault.GetEntryAsync(MediaVaultType.Audio, new EntryKey(trackId, MediaVaultType.Audio)); + if (file == null) { return NotFound(); } + return File(file.Buffer, MimeTypeExtensions.GetMimeType(file.Extension)); + } +} \ No newline at end of file diff --git a/DeepDrftContent/FileDatabase/Abstractions/IIndexFactory.cs b/DeepDrftContent/FileDatabase/Abstractions/IIndexFactory.cs new file mode 100644 index 0000000..6d7a66f --- /dev/null +++ b/DeepDrftContent/FileDatabase/Abstractions/IIndexFactory.cs @@ -0,0 +1,41 @@ +using DeepDrftContent.FileDatabase.Models; +using IndexType = DeepDrftContent.FileDatabase.Services.IndexType; + +namespace DeepDrftContent.FileDatabase.Abstractions; + +/// +/// Interface for creating index instances +/// +public interface IIndexFactory +{ + /// + /// Creates an index of the specified type + /// + Task CreateIndexAsync(IndexType type, string rootPath); + + /// + /// Loads an existing index of the specified type + /// + Task LoadIndexAsync(IndexType type, string rootPath); + + /// + /// Loads existing index or creates new one if loading fails + /// + Task LoadOrCreateIndexAsync(IndexType type, string rootPath); +} + +/// +/// Interface for creating index data objects +/// +public interface IIndexDataFactory +{ + /// + /// Creates index data for serialization + /// + object CreateIndexData(IndexType type, IIndex index); + + /// + /// Creates index instance from data + /// + IIndex CreateIndexFromData(IndexType type, object indexData); +} diff --git a/DeepDrftContent/FileDatabase/Abstractions/IMediaTypeRegistry.cs b/DeepDrftContent/FileDatabase/Abstractions/IMediaTypeRegistry.cs new file mode 100644 index 0000000..749c3d2 --- /dev/null +++ b/DeepDrftContent/FileDatabase/Abstractions/IMediaTypeRegistry.cs @@ -0,0 +1,69 @@ +using DeepDrftContent.FileDatabase.Models; +using DeepDrftContent.FileDatabase.Services; + +namespace DeepDrftContent.FileDatabase.Abstractions; + +/// +/// Interface for registering media type factories +/// +public interface IMediaTypeRegistry +{ + /// + /// Register a factory for a specific media vault type + /// + void RegisterMediaType(MediaVaultType vaultType) + where TBinary : FileBinary + where TParams : FileBinaryParams + where TDto : FileBinaryDto + where TMetaData : MetaData; + + /// + /// Create a binary object from parameters + /// + FileBinary CreateBinary(MediaVaultType vaultType, object parameters); + + /// + /// Create a binary object from DTO + /// + FileBinary CreateBinaryFromDto(MediaVaultType vaultType, object dto); + + /// + /// Create a DTO from binary object + /// + FileBinaryDto CreateDto(MediaVaultType vaultType, FileBinary binary); + + /// + /// Create metadata from media object + /// + MetaData CreateMetaDataFromMedia(MediaVaultType vaultType, string entryKey, string extension, object media); + + /// + /// Create parameters from binary and metadata + /// + object CreateParams(MediaVaultType vaultType, FileBinary fileBinary, MetaData metaData); + + /// + /// Create media vault + /// + Task CreateVaultAsync(MediaVaultType vaultType, string rootPath); + + /// + /// Get the binary type for a vault type + /// + Type GetBinaryType(MediaVaultType vaultType); + + /// + /// Get the DTO type for a vault type + /// + Type GetDtoType(MediaVaultType vaultType); + + /// + /// Get the parameters type for a vault type + /// + Type GetParamsType(MediaVaultType vaultType); + + /// + /// Get the metadata type for a vault type + /// + Type GetMetaDataType(MediaVaultType vaultType); +} diff --git a/DeepDrftContent/FileDatabase/Models/IIndex.cs b/DeepDrftContent/FileDatabase/Models/IIndex.cs index 867f3e1..24ebcbd 100644 --- a/DeepDrftContent/FileDatabase/Models/IIndex.cs +++ b/DeepDrftContent/FileDatabase/Models/IIndex.cs @@ -1,7 +1,7 @@ namespace DeepDrftContent.FileDatabase.Models; /// -/// Base interface for all index types +/// Base interface for all index types - minimal contract /// public interface IIndex { @@ -9,7 +9,13 @@ public interface IIndex /// Gets the key identifier for this index /// string GetKey(); +} +/// +/// Interface for indexes that support entry queries +/// +public interface IEntryQueryable : IIndex +{ /// /// Gets all entry keys in this index /// @@ -25,3 +31,30 @@ public interface IIndex /// bool HasEntry(EntryKey entryKey); } + +/// +/// Interface for indexes that support directory operations +/// +public interface IDirectoryIndex : IEntryQueryable +{ + /// + /// Adds an entry to the directory index + /// + void PutEntry(EntryKey entryKey); +} + +/// +/// Interface for indexes that support vault operations with metadata +/// +public interface IVaultIndex : IEntryQueryable +{ + /// + /// Gets metadata for a specific entry + /// + MetaData? GetEntry(EntryKey entryKey); + + /// + /// Adds an entry with metadata to the vault index + /// + void PutEntry(EntryKey entryKey, MetaData metaData); +} diff --git a/DeepDrftContent/FileDatabase/Models/IndexData.cs b/DeepDrftContent/FileDatabase/Models/IndexData.cs index 1b79047..9e47454 100644 --- a/DeepDrftContent/FileDatabase/Models/IndexData.cs +++ b/DeepDrftContent/FileDatabase/Models/IndexData.cs @@ -65,7 +65,7 @@ public class VaultIndexData : IndexData /// /// Directory index implementation using StructuralSet for entries /// -public class DirectoryIndex : IndexData, IIndex +public class DirectoryIndex : IndexData, IDirectoryIndex { public StructuralSet Entries { get; } @@ -93,7 +93,7 @@ public class DirectoryIndex : IndexData, IIndex /// /// Vault index implementation using StructuralMap for entries with metadata /// -public class VaultIndex : IndexData, IIndex +public class VaultIndex : IndexData, IVaultIndex { public StructuralMap Entries { get; } diff --git a/DeepDrftContent/FileDatabase/Models/MediaFactories.cs b/DeepDrftContent/FileDatabase/Models/MediaFactories.cs index 1db85eb..674c622 100644 --- a/DeepDrftContent/FileDatabase/Models/MediaFactories.cs +++ b/DeepDrftContent/FileDatabase/Models/MediaFactories.cs @@ -1,37 +1,22 @@ +using DeepDrftContent.FileDatabase.Abstractions; +using DeepDrftContent.FileDatabase.Services; + namespace DeepDrftContent.FileDatabase.Models; /// -/// Type mappings for media vault types to their corresponding classes +/// Type mappings for media vault types - simple dictionary-based approach /// public static class MediaVaultTypeMap { - public static Type GetBinaryType(MediaVaultType vaultType) => vaultType switch - { - MediaVaultType.Media => typeof(MediaBinary), - MediaVaultType.Image => typeof(ImageBinary), - _ => throw new ArgumentException($"Unknown vault type: {vaultType}") - }; + private static readonly IMediaTypeRegistry _registry = new SimpleMediaTypeRegistry(); - public static Type GetDtoType(MediaVaultType vaultType) => vaultType switch - { - MediaVaultType.Media => typeof(MediaBinaryDto), - MediaVaultType.Image => typeof(ImageBinaryDto), - _ => throw new ArgumentException($"Unknown vault type: {vaultType}") - }; + public static Type GetBinaryType(MediaVaultType vaultType) => _registry.GetBinaryType(vaultType); - public static Type GetParamsType(MediaVaultType vaultType) => vaultType switch - { - MediaVaultType.Media => typeof(MediaBinaryParams), - MediaVaultType.Image => typeof(ImageBinaryParams), - _ => throw new ArgumentException($"Unknown vault type: {vaultType}") - }; + public static Type GetDtoType(MediaVaultType vaultType) => _registry.GetDtoType(vaultType); - public static Type GetMetaDataType(MediaVaultType vaultType) => vaultType switch - { - MediaVaultType.Media => typeof(MetaData), - MediaVaultType.Image => typeof(ImageMetaData), - _ => throw new ArgumentException($"Unknown vault type: {vaultType}") - }; + public static Type GetParamsType(MediaVaultType vaultType) => _registry.GetParamsType(vaultType); + + public static Type GetMetaDataType(MediaVaultType vaultType) => _registry.GetMetaDataType(vaultType); } /// @@ -39,38 +24,52 @@ public static class MediaVaultTypeMap /// public static class MetaDataFactory { - public static MetaData Create(MediaVaultType type, string entryKey, string extension, double aspectRatio = 1.0) + public static MetaData Create(MediaVaultType type, string entryKey, string extension) { return type switch { MediaVaultType.Media => new MetaData(entryKey, extension), - MediaVaultType.Image => new ImageMetaData(entryKey, extension, aspectRatio), + MediaVaultType.Image => throw new ArgumentException("Image metadata requires aspect ratio. Use CreateImageMetaData instead."), + MediaVaultType.Audio => throw new ArgumentException("Audio metadata requires duration and bitrate. Use CreateAudioMetaData instead."), _ => throw new ArgumentException($"Unknown vault type: {type}") }; } - public static T Create(MediaVaultType type, string entryKey, string extension, double aspectRatio = 1.0) + public static ImageMetaData CreateImageMetaData(string entryKey, string extension, double aspectRatio) + { + return new ImageMetaData(entryKey, extension, aspectRatio); + } + + public static AudioMetaData CreateAudioMetaData(string entryKey, string extension, double duration, int bitrate) + { + return new AudioMetaData(entryKey, extension, duration, bitrate); + } + + private static readonly IMediaTypeRegistry _metaDataRegistry = new SimpleMediaTypeRegistry(); + + public static MetaData CreateFromMedia(MediaVaultType type, string entryKey, string extension, object media) + { + return _metaDataRegistry.CreateMetaDataFromMedia(type, entryKey, extension, media); + } + + public static T Create(MediaVaultType type, string entryKey, string extension) where T : MetaData { - var metaData = Create(type, entryKey, extension, aspectRatio); + var metaData = Create(type, entryKey, extension); return (T)metaData; } } /// -/// Factory for creating media parameter objects +/// Factory for creating media parameter objects - simple dictionary-based approach /// public static class MediaParamsFactory { + private static readonly IMediaTypeRegistry _registry = new SimpleMediaTypeRegistry(); + public static object Create(MediaVaultType type, FileBinary fileBinary, MetaData metaData) { - return type switch - { - MediaVaultType.Media => new MediaBinaryParams(fileBinary.Buffer, fileBinary.Size, metaData.Extension), - MediaVaultType.Image when metaData is ImageMetaData imageMetaData => - new ImageBinaryParams(fileBinary.Buffer, fileBinary.Size, metaData.Extension, imageMetaData.AspectRatio), - _ => throw new ArgumentException($"Invalid vault type {type} or metadata type mismatch") - }; + return _registry.CreateParams(type, fileBinary, metaData); } public static T Create(MediaVaultType type, FileBinary fileBinary, MetaData metaData) @@ -81,20 +80,15 @@ public static class MediaParamsFactory } /// -/// Factory for creating media binary objects from parameters +/// Factory for creating media binary objects - simple dictionary-based approach /// public static class FileBinaryFactory { + private static readonly IMediaTypeRegistry _registry = new SimpleMediaTypeRegistry(); + public static object Create(MediaVaultType vaultType, object parameters) { - return vaultType switch - { - MediaVaultType.Media when parameters is MediaBinaryParams mediaParams => - new MediaBinary(mediaParams), - MediaVaultType.Image when parameters is ImageBinaryParams imageParams => - new ImageBinary(imageParams), - _ => throw new ArgumentException($"Invalid vault type {vaultType} or parameter type mismatch") - }; + return _registry.CreateBinary(vaultType, parameters); } public static T Create(MediaVaultType vaultType, object parameters) where T : FileBinary @@ -105,14 +99,7 @@ public static class FileBinaryFactory public static object From(MediaVaultType type, object mediaBinaryDto) { - return type switch - { - MediaVaultType.Media when mediaBinaryDto is MediaBinaryDto mediaDto => - MediaBinary.From(mediaDto), - MediaVaultType.Image when mediaBinaryDto is ImageBinaryDto imageDto => - ImageBinary.From(imageDto), - _ => throw new ArgumentException($"Invalid type {type} or DTO type mismatch") - }; + return _registry.CreateBinaryFromDto(type, mediaBinaryDto); } public static T From(MediaVaultType type, object mediaBinaryDto) where T : FileBinary @@ -123,20 +110,18 @@ public static class FileBinaryFactory } /// -/// Factory for creating DTO objects from media binaries +/// Factory for creating DTO objects from media binaries - simple dictionary-based approach /// public static class FileBinaryDtoFactory { + private static readonly IMediaTypeRegistry _registry = new SimpleMediaTypeRegistry(); + public static object From(MediaVaultType type, object mediaBinary) { - return type switch - { - MediaVaultType.Media when mediaBinary is MediaBinary media => - new MediaBinaryDto(media), - MediaVaultType.Image when mediaBinary is ImageBinary image => - new ImageBinaryDto(image), - _ => throw new ArgumentException($"Invalid type {type} or binary type mismatch") - }; + if (mediaBinary is not FileBinary fileBinary) + throw new ArgumentException($"Expected FileBinary but got {mediaBinary.GetType()}"); + + return _registry.CreateDto(type, fileBinary); } public static T From(MediaVaultType type, object mediaBinary) diff --git a/DeepDrftContent/FileDatabase/Models/MediaModels.cs b/DeepDrftContent/FileDatabase/Models/MediaModels.cs index ebaaee3..6bb0ecf 100644 --- a/DeepDrftContent/FileDatabase/Models/MediaModels.cs +++ b/DeepDrftContent/FileDatabase/Models/MediaModels.cs @@ -140,6 +140,63 @@ public record ImageBinaryDto(string Base64, int Size, string Mime, double Aspect imageBinary.AspectRatio) { } } +/// +/// Parameters for creating an AudioBinary +/// +/// The binary data +/// The size of the data in bytes +/// The file extension +/// The duration of the audio in seconds +/// The bitrate of the audio in kbps +public record AudioBinaryParams(byte[] Buffer, int Size, string Extension, double Duration, int Bitrate) + : MediaBinaryParams(Buffer, Size, Extension); + +/// +/// Audio binary with duration and bitrate information +/// +public class AudioBinary : MediaBinary +{ + public double Duration { get; } + public int Bitrate { get; } + + public AudioBinary(AudioBinaryParams parameters) : base(parameters) + { + Duration = parameters.Duration; + Bitrate = parameters.Bitrate; + } + + public static AudioBinary From(AudioBinaryDto dto) + { + var buffer = Convert.FromBase64String(dto.Base64); + var extension = GetExtensionType(dto.Mime); + return new AudioBinary(new AudioBinaryParams(buffer, dto.Size, extension, dto.Duration, dto.Bitrate)); + } + + private static string GetExtensionType(string mime) + { + return MimeTypeExtensions.GetExtension(mime); + } +} + +/// +/// DTO for AudioBinary serialization +/// +/// Base64 encoded binary data +/// Size of the original data +/// MIME type of the media +/// The duration of the audio in seconds +/// The bitrate of the audio in kbps +public record AudioBinaryDto(string Base64, int Size, string Mime, double Duration, int Bitrate) + : MediaBinaryDto(Base64, Size, Mime) +{ + public AudioBinaryDto(AudioBinary audioBinary) : this( + Convert.ToBase64String(audioBinary.Buffer), + audioBinary.Size, + MimeTypeExtensions.GetMimeType(audioBinary.Extension), + audioBinary.Duration, + audioBinary.Bitrate) { } +} + /// /// Utility class for MIME type and extension conversions /// @@ -153,7 +210,13 @@ public static class MimeTypeExtensions { ".gif", "image/gif" }, { ".webp", "image/webp" }, { ".svg", "image/svg+xml" }, - { ".bmp", "image/bmp" } + { ".bmp", "image/bmp" }, + { ".mp3", "audio/mpeg" }, + { ".wav", "audio/wav" }, + { ".flac", "audio/flac" }, + { ".aac", "audio/aac" }, + { ".ogg", "audio/ogg" }, + { ".m4a", "audio/mp4" } }; private static readonly Dictionary Extensions = new() @@ -163,7 +226,13 @@ public static class MimeTypeExtensions { "image/gif", ".gif" }, { "image/webp", ".webp" }, { "image/svg+xml", ".svg" }, - { "image/bmp", ".bmp" } + { "image/bmp", ".bmp" }, + { "audio/mpeg", ".mp3" }, + { "audio/wav", ".wav" }, + { "audio/flac", ".flac" }, + { "audio/aac", ".aac" }, + { "audio/ogg", ".ogg" }, + { "audio/mp4", ".m4a" } }; public static string GetMimeType(string extension) diff --git a/DeepDrftContent/FileDatabase/Models/MediaVaultType.cs b/DeepDrftContent/FileDatabase/Models/MediaVaultType.cs index 3ed2263..a5f979a 100644 --- a/DeepDrftContent/FileDatabase/Models/MediaVaultType.cs +++ b/DeepDrftContent/FileDatabase/Models/MediaVaultType.cs @@ -6,5 +6,6 @@ namespace DeepDrftContent.FileDatabase.Models; public enum MediaVaultType { Media, - Image + Image, + Audio } diff --git a/DeepDrftContent/FileDatabase/Models/MetaData.cs b/DeepDrftContent/FileDatabase/Models/MetaData.cs index be1f7e0..9f8225c 100644 --- a/DeepDrftContent/FileDatabase/Models/MetaData.cs +++ b/DeepDrftContent/FileDatabase/Models/MetaData.cs @@ -10,6 +10,7 @@ namespace DeepDrftContent.FileDatabase.Models; [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] [JsonDerivedType(typeof(MetaData), typeDiscriminator: "media")] [JsonDerivedType(typeof(ImageMetaData), typeDiscriminator: "image")] +[JsonDerivedType(typeof(AudioMetaData), typeDiscriminator: "audio")] public record MetaData(string MediaKey, string Extension); /// @@ -20,3 +21,13 @@ public record MetaData(string MediaKey, string Extension); /// The aspect ratio of the image public record ImageMetaData(string MediaKey, string Extension, double AspectRatio) : MetaData(MediaKey, Extension); + +/// +/// Extended metadata for audio entries, including duration and bitrate +/// +/// The key used to identify the media file +/// The file extension of the media +/// The duration of the audio in seconds +/// The bitrate of the audio in kbps +public record AudioMetaData(string MediaKey, string Extension, double Duration, int Bitrate) + : MetaData(MediaKey, Extension); diff --git a/DeepDrftContent/FileDatabase/Services/FileDatabase.cs b/DeepDrftContent/FileDatabase/Services/FileDatabase.cs index 09613d3..8abcef1 100644 --- a/DeepDrftContent/FileDatabase/Services/FileDatabase.cs +++ b/DeepDrftContent/FileDatabase/Services/FileDatabase.cs @@ -50,7 +50,7 @@ public class FileDatabase : DirectoryIndexDirectory private async Task InitVaultAsync(EntryKey vaultKey) { var path = Path.Combine(RootPath, vaultKey.Key); - var directoryVault = await ImageDirectoryVault.FromAsync(path); + var directoryVault = await MediaVaultFactory.From(path, vaultKey.Type); if (directoryVault != null) { @@ -82,7 +82,7 @@ public class FileDatabase : DirectoryIndexDirectory try { var path = Path.Combine(RootPath, vaultKey.Key); - var directoryVault = await ImageDirectoryVault.FromAsync(path); + var directoryVault = await MediaVaultFactory.From(path, vaultKey.Type); if (directoryVault != null) { @@ -92,7 +92,6 @@ public class FileDatabase : DirectoryIndexDirectory } catch { - // Re-throw to maintain the same error behavior as TypeScript version throw; } } diff --git a/DeepDrftContent/FileDatabase/Services/IndexFactoryService.cs b/DeepDrftContent/FileDatabase/Services/IndexFactoryService.cs new file mode 100644 index 0000000..c3d07bb --- /dev/null +++ b/DeepDrftContent/FileDatabase/Services/IndexFactoryService.cs @@ -0,0 +1,111 @@ +using DeepDrftContent.FileDatabase.Abstractions; +using DeepDrftContent.FileDatabase.Models; +using DeepDrftContent.FileDatabase.Utils; +using IndexType = DeepDrftContent.FileDatabase.Services.IndexType; + +namespace DeepDrftContent.FileDatabase.Services; + +/// +/// Factory service for creating and managing indexes +/// +public class IndexFactoryService : IIndexFactory, IIndexDataFactory +{ + private readonly Dictionary> _indexCreators; + private readonly Dictionary> _indexFromDataCreators; + private readonly Dictionary> _indexDataCreators; + + public IndexFactoryService() + { + _indexCreators = new Dictionary> + { + { IndexType.Directory, rootPath => new DirectoryIndex(new DirectoryIndexData(Path.GetFileName(rootPath))) }, + { IndexType.Vault, rootPath => new VaultIndex(new VaultIndexData(Path.GetFileName(rootPath))) } + }; + + _indexFromDataCreators = new Dictionary> + { + { IndexType.Directory, data => new DirectoryIndex((DirectoryIndexData)data) }, + { IndexType.Vault, data => new VaultIndex((VaultIndexData)data) } + }; + + _indexDataCreators = new Dictionary> + { + { IndexType.Directory, index => DirectoryIndexData.FromIndex((DirectoryIndex)index) }, + { IndexType.Vault, index => VaultIndexData.FromIndex((VaultIndex)index) } + }; + } + + public async Task CreateIndexAsync(IndexType type, string rootPath) + { + if (!_indexCreators.TryGetValue(type, out var creator)) + { + throw new ArgumentException($"Unknown index type: {type}"); + } + + var index = creator(rootPath); + + // Ensure directory exists and save the index + await FileUtils.MakeVaultDirectoryAsync(rootPath); + await SaveIndexAsync(rootPath, type, index); + + return index; + } + + public async Task LoadIndexAsync(IndexType type, string rootPath) + { + if (!_indexFromDataCreators.TryGetValue(type, out var creator)) + { + throw new ArgumentException($"Unknown index type: {type}"); + } + + var indexPath = Path.Combine(rootPath, "index"); + + object indexData = type switch + { + IndexType.Directory => await FileUtils.FetchObjectAsync(indexPath), + IndexType.Vault => await FileUtils.FetchObjectAsync(indexPath), + _ => throw new ArgumentException($"Unknown index type: {type}") + }; + + return creator(indexData); + } + + public async Task LoadOrCreateIndexAsync(IndexType type, string rootPath) + { + try + { + return await LoadIndexAsync(type, rootPath); + } + catch + { + return await CreateIndexAsync(type, rootPath); + } + } + + public object CreateIndexData(IndexType type, IIndex index) + { + if (!_indexDataCreators.TryGetValue(type, out var creator)) + { + throw new ArgumentException($"Unknown index type: {type}"); + } + + return creator(index); + } + + public IIndex CreateIndexFromData(IndexType type, object indexData) + { + if (!_indexFromDataCreators.TryGetValue(type, out var creator)) + { + throw new ArgumentException($"Unknown index type: {type}"); + } + + return creator(indexData); + } + + private async Task SaveIndexAsync(string rootPath, IndexType type, IIndex index) + { + var indexPath = Path.Combine(rootPath, "index"); + var indexData = CreateIndexData(type, index); + await FileUtils.PutObjectAsync(indexPath, indexData); + } +} diff --git a/DeepDrftContent/FileDatabase/Services/IndexSystem.cs b/DeepDrftContent/FileDatabase/Services/IndexSystem.cs index 76fc656..1b0401c 100644 --- a/DeepDrftContent/FileDatabase/Services/IndexSystem.cs +++ b/DeepDrftContent/FileDatabase/Services/IndexSystem.cs @@ -1,3 +1,4 @@ +using DeepDrftContent.FileDatabase.Abstractions; using DeepDrftContent.FileDatabase.Models; using DeepDrftContent.FileDatabase.Utils; @@ -19,11 +20,13 @@ public abstract class AbstractIndexContainer { protected IndexType Type { get; } public string RootPath { get; } + private readonly IIndexDataFactory _indexDataFactory; - protected AbstractIndexContainer(string path, IndexType type) + protected AbstractIndexContainer(string path, IndexType type, IIndexDataFactory? indexDataFactory = null) { RootPath = path; Type = type; + _indexDataFactory = indexDataFactory ?? new IndexFactoryService(); } public string GetKey() => Path.GetFileName(RootPath); @@ -31,78 +34,30 @@ public abstract class AbstractIndexContainer protected async Task SaveIndexAsync(T index) where T : IIndex { var indexPath = Path.Combine(RootPath, "index"); - - object indexData = Type switch - { - IndexType.Directory when index is DirectoryIndex dirIndex => DirectoryIndexData.FromIndex(dirIndex), - IndexType.Vault when index is VaultIndex vaultIndex => VaultIndexData.FromIndex(vaultIndex), - _ => throw new ArgumentException($"Invalid index type {Type} for index {typeof(T)}") - }; - + var indexData = _indexDataFactory.CreateIndexData(Type, index); await FileUtils.PutObjectAsync(indexPath, indexData); } } /// -/// Factory for creating and loading indexes +/// Factory for creating and loading indexes - delegates to IIndexFactory /// public class IndexFactory : AbstractIndexContainer { - public IndexFactory(string path, IndexType type) : base(path, type) { } + private readonly IIndexFactory _factoryService; + + public IndexFactory(string path, IndexType type, IIndexFactory? factoryService = null, IIndexDataFactory? indexDataFactory = null) + : base(path, type, indexDataFactory) + { + _factoryService = factoryService ?? new IndexFactoryService(); + } /// /// Builds an index by loading existing or creating new /// public async Task BuildIndexAsync() { - try - { - return await LoadOrCreateIndexAsync(); - } - catch - { - return null; - } - } - - private async Task LoadOrCreateIndexAsync() - { - try - { - return await LoadIndexAsync(); - } - catch - { - return await CreateIndexAsync(); - } - } - - private async Task LoadIndexAsync() - { - var indexPath = Path.Combine(RootPath, "index"); - - IIndex result = Type switch - { - IndexType.Directory => new DirectoryIndex(await FileUtils.FetchObjectAsync(indexPath)), - IndexType.Vault => new VaultIndex(await FileUtils.FetchObjectAsync(indexPath)), - _ => throw new ArgumentException($"Unknown index type: {Type}") - }; - return result; - } - - private async Task CreateIndexAsync() - { - IIndex index = Type switch - { - IndexType.Directory => new DirectoryIndex(new DirectoryIndexData(RootPath)), - IndexType.Vault => new VaultIndex(new VaultIndexData(RootPath)), - _ => throw new ArgumentException($"Unknown index type: {Type}") - }; - - await FileUtils.MakeVaultDirectoryAsync(RootPath); - await SaveIndexAsync(index); - - return index; + return await _factoryService.LoadOrCreateIndexAsync(Type, RootPath); } } @@ -111,9 +66,10 @@ public class IndexFactory : AbstractIndexContainer /// public abstract class IndexDirectory : AbstractIndexContainer { - protected IIndex Index { get; } + protected IEntryQueryable Index { get; } - protected IndexDirectory(string rootPath, IndexType type, IIndex index) : base(rootPath, type) + protected IndexDirectory(string rootPath, IndexType type, IEntryQueryable index, IIndexDataFactory? indexDataFactory = null) + : base(rootPath, type, indexDataFactory) { Index = index; } @@ -130,16 +86,18 @@ public abstract class IndexDirectory : AbstractIndexContainer /// public class DirectoryIndexDirectory : IndexDirectory { - public DirectoryIndexDirectory(string rootPath, DirectoryIndex index) - : base(rootPath, IndexType.Directory, index) { } + private readonly IDirectoryIndex _directoryIndex; + + public DirectoryIndexDirectory(string rootPath, IDirectoryIndex index, IIndexDataFactory? indexDataFactory = null) + : base(rootPath, IndexType.Directory, index, indexDataFactory) + { + _directoryIndex = index; + } protected async Task AddToIndexAsync(EntryKey entryKey) { - if (Index is DirectoryIndex dirIndex) - { - dirIndex.PutEntry(entryKey); - await SaveIndexAsync(dirIndex); - } + _directoryIndex.PutEntry(entryKey); + await SaveIndexAsync(_directoryIndex); } } @@ -148,15 +106,17 @@ public class DirectoryIndexDirectory : IndexDirectory /// public class VaultIndexDirectory : IndexDirectory { - public VaultIndexDirectory(string rootPath, VaultIndex index) - : base(rootPath, IndexType.Vault, index) { } + private readonly IVaultIndex _vaultIndex; + + public VaultIndexDirectory(string rootPath, IVaultIndex index, IIndexDataFactory? indexDataFactory = null) + : base(rootPath, IndexType.Vault, index, indexDataFactory) + { + _vaultIndex = index; + } protected async Task AddToIndexAsync(EntryKey entryKey, MetaData metaData) { - if (Index is VaultIndex vaultIndex) - { - vaultIndex.PutEntry(entryKey, metaData); - await SaveIndexAsync(vaultIndex); - } + _vaultIndex.PutEntry(entryKey, metaData); + await SaveIndexAsync(_vaultIndex); } } diff --git a/DeepDrftContent/FileDatabase/Services/MediaVault.cs b/DeepDrftContent/FileDatabase/Services/MediaVault.cs index 54027c9..7b52f55 100644 --- a/DeepDrftContent/FileDatabase/Services/MediaVault.cs +++ b/DeepDrftContent/FileDatabase/Services/MediaVault.cs @@ -45,7 +45,7 @@ public abstract class MediaVault : VaultIndexDirectory var (buffer, extension) = ExtractMediaProperties(media); var mediaPath = GetMediaPathFromEntryKey(entryKey.Key, extension); - var metaData = MetaDataFactory.Create(vaultType, entryKey.Key, extension, GetAspectRatio(media)); + var metaData = MetaDataFactory.CreateFromMedia(vaultType, entryKey.Key, extension, media); await AddToIndexAsync(entryKey, metaData); await FileUtils.PutFileAsync(mediaPath, buffer); @@ -86,42 +86,51 @@ public abstract class MediaVault : VaultIndexDirectory return media switch { ImageBinary imageBinary => (imageBinary.Buffer, imageBinary.Extension), + AudioBinary audioBinary => (audioBinary.Buffer, audioBinary.Extension), MediaBinary mediaBinary => (mediaBinary.Buffer, mediaBinary.Extension), _ => throw new ArgumentException($"Unsupported media type: {media.GetType()}") }; } - - /// - /// Extracts aspect ratio from media object if it's an image - /// - private static double GetAspectRatio(object media) - { - return media is ImageBinary imageBinary ? imageBinary.AspectRatio : 1.0; - } } /// /// Concrete implementation of MediaVault for image storage /// -public class ImageDirectoryVault : MediaVault +public class ImageVault : MediaVault { - private ImageDirectoryVault(string rootPath, VaultIndex index) : base(rootPath, index) { } + private ImageVault(string rootPath, VaultIndex index) : base(rootPath, index) { } /// - /// Factory method to create an ImageDirectoryVault instance + /// Factory method to create an ImageVault instance /// - public static async Task FromAsync(string rootPath) + public static async Task FromAsync(string rootPath) { var factory = new IndexFactory(rootPath, IndexType.Vault); var index = await factory.BuildIndexAsync(); if (index is VaultIndex vaultIndex) { - return new ImageDirectoryVault(rootPath, vaultIndex); + return new ImageVault(rootPath, vaultIndex); + } + + return null; + } +} + +public class AudioVault : MediaVault +{ + private AudioVault(string rootPath, VaultIndex index) : base(rootPath, index) { } + + public static async Task FromAsync(string rootPath) + { + var factory = new IndexFactory(rootPath, IndexType.Vault); + var index = await factory.BuildIndexAsync(); + + if (index is VaultIndex vaultIndex) + { + return new AudioVault(rootPath, vaultIndex); } return null; } - - } diff --git a/DeepDrftContent/FileDatabase/Services/MediaVaultFactory.cs b/DeepDrftContent/FileDatabase/Services/MediaVaultFactory.cs new file mode 100644 index 0000000..bc1c819 --- /dev/null +++ b/DeepDrftContent/FileDatabase/Services/MediaVaultFactory.cs @@ -0,0 +1,17 @@ +using DeepDrftContent.FileDatabase.Abstractions; +using DeepDrftContent.FileDatabase.Models; + +namespace DeepDrftContent.FileDatabase.Services; + +/// +/// Factory for creating media vaults - simple dictionary-based approach +/// +public static class MediaVaultFactory +{ + private static readonly IMediaTypeRegistry _registry = new SimpleMediaTypeRegistry(); + + public static async Task From(string rootPath, MediaVaultType mediaType) + { + return await _registry.CreateVaultAsync(mediaType, rootPath); + } +} \ No newline at end of file diff --git a/DeepDrftContent/FileDatabase/Services/SimpleMediaTypeRegistry.cs b/DeepDrftContent/FileDatabase/Services/SimpleMediaTypeRegistry.cs new file mode 100644 index 0000000..1413021 --- /dev/null +++ b/DeepDrftContent/FileDatabase/Services/SimpleMediaTypeRegistry.cs @@ -0,0 +1,149 @@ +using DeepDrftContent.FileDatabase.Abstractions; +using DeepDrftContent.FileDatabase.Models; + +namespace DeepDrftContent.FileDatabase.Services; + +/// +/// Simple dictionary-based registry for media type factories +/// +public class SimpleMediaTypeRegistry : IMediaTypeRegistry +{ + private readonly Dictionary> _binaryFactories = new(); + private readonly Dictionary> _binaryFromDtoFactories = new(); + private readonly Dictionary> _dtoFactories = new(); + private readonly Dictionary> _metaDataFromMediaFactories = new(); + private readonly Dictionary> _paramsFactories = new(); + private readonly Dictionary>> _vaultFactories = new(); + private readonly Dictionary _binaryTypes = new(); + private readonly Dictionary _dtoTypes = new(); + private readonly Dictionary _paramsTypes = new(); + private readonly Dictionary _metaDataTypes = new(); + + public SimpleMediaTypeRegistry() + { + // Clean one-line registrations with generics - no reflection! + RegisterType( + MediaVaultType.Media, + p => new MediaBinary(p), + dto => MediaBinary.From(dto), + binary => new MediaBinaryDto(binary), + (key, ext, _) => new MetaData(key, ext), + (binary, meta) => new MediaBinaryParams(binary.Buffer, binary.Size, meta.Extension)); + + RegisterType( + MediaVaultType.Image, + p => new ImageBinary(p), + dto => ImageBinary.From(dto), + binary => new ImageBinaryDto(binary), + (key, ext, media) => media is ImageBinary img ? new ImageMetaData(key, ext, img.AspectRatio) : new MetaData(key, ext), + (binary, meta) => meta is ImageMetaData imgMeta + ? new ImageBinaryParams(binary.Buffer, binary.Size, meta.Extension, imgMeta.AspectRatio) + : throw new ArgumentException("ImageBinary requires ImageMetaData"), + async path => await ImageVault.FromAsync(path)); + + RegisterType( + MediaVaultType.Audio, + p => new AudioBinary(p), + dto => AudioBinary.From(dto), + binary => new AudioBinaryDto(binary), + (key, ext, media) => media is AudioBinary audio ? new AudioMetaData(key, ext, audio.Duration, audio.Bitrate) : new MetaData(key, ext), + (binary, meta) => meta is AudioMetaData audioMeta + ? new AudioBinaryParams(binary.Buffer, binary.Size, meta.Extension, audioMeta.Duration, audioMeta.Bitrate) + : throw new ArgumentException("AudioBinary requires AudioMetaData"), + async path => await AudioVault.FromAsync(path)); + } + + private void RegisterType( + MediaVaultType vaultType, + Func binaryFactory, + Func binaryFromDtoFactory, + Func dtoFactory, + Func metaDataFactory, + Func paramsFactory, + Func>? vaultFactory = null) + where TBinary : FileBinary + where TParams : FileBinaryParams + where TDto : FileBinaryDto + where TMetaData : MetaData + { + _binaryFactories[vaultType] = p => binaryFactory((TParams)p); + _binaryFromDtoFactories[vaultType] = dto => binaryFromDtoFactory((TDto)dto); + _dtoFactories[vaultType] = binary => dtoFactory((TBinary)binary); + _metaDataFromMediaFactories[vaultType] = metaDataFactory; + _paramsFactories[vaultType] = paramsFactory; + _binaryTypes[vaultType] = typeof(TBinary); + _dtoTypes[vaultType] = typeof(TDto); + _paramsTypes[vaultType] = typeof(TParams); + _metaDataTypes[vaultType] = typeof(TMetaData); + + if (vaultFactory != null) + _vaultFactories[vaultType] = vaultFactory; + } + + // Public interface implementation - allows external registration + public void RegisterMediaType(MediaVaultType vaultType) + where TBinary : FileBinary + where TParams : FileBinaryParams + where TDto : FileBinaryDto + where TMetaData : MetaData + { + // For now, we can't auto-generate the factories without reflection + // This would need to be implemented if external registration is needed + throw new NotImplementedException("Use RegisterType method for internal registration. External registration not yet implemented."); + } + + + public FileBinary CreateBinary(MediaVaultType vaultType, object parameters) + { + return _binaryFactories.TryGetValue(vaultType, out var factory) + ? factory(parameters) + : throw new ArgumentException($"Unknown vault type: {vaultType}"); + } + + public FileBinary CreateBinaryFromDto(MediaVaultType vaultType, object dto) + { + return _binaryFromDtoFactories.TryGetValue(vaultType, out var factory) + ? factory(dto) + : throw new ArgumentException($"Unknown vault type: {vaultType}"); + } + + public FileBinaryDto CreateDto(MediaVaultType vaultType, FileBinary binary) + { + return _dtoFactories.TryGetValue(vaultType, out var factory) + ? factory(binary) + : throw new ArgumentException($"Unknown vault type: {vaultType}"); + } + + public MetaData CreateMetaDataFromMedia(MediaVaultType vaultType, string entryKey, string extension, object media) + { + return _metaDataFromMediaFactories.TryGetValue(vaultType, out var factory) + ? factory(entryKey, extension, media) + : throw new ArgumentException($"Unknown vault type: {vaultType}"); + } + + public object CreateParams(MediaVaultType vaultType, FileBinary fileBinary, MetaData metaData) + { + return _paramsFactories.TryGetValue(vaultType, out var factory) + ? factory(fileBinary, metaData) + : throw new ArgumentException($"Unknown vault type: {vaultType}"); + } + + public async Task CreateVaultAsync(MediaVaultType vaultType, string rootPath) + { + return _vaultFactories.TryGetValue(vaultType, out var factory) + ? await factory(rootPath) + : null; + } + + public Type GetBinaryType(MediaVaultType vaultType) => + _binaryTypes.TryGetValue(vaultType, out var type) ? type : throw new ArgumentException($"Unknown vault type: {vaultType}"); + + public Type GetDtoType(MediaVaultType vaultType) => + _dtoTypes.TryGetValue(vaultType, out var type) ? type : throw new ArgumentException($"Unknown vault type: {vaultType}"); + + public Type GetParamsType(MediaVaultType vaultType) => + _paramsTypes.TryGetValue(vaultType, out var type) ? type : throw new ArgumentException($"Unknown vault type: {vaultType}"); + + public Type GetMetaDataType(MediaVaultType vaultType) => + _metaDataTypes.TryGetValue(vaultType, out var type) ? type : throw new ArgumentException($"Unknown vault type: {vaultType}"); +} diff --git a/DeepDrftContent/FileDatabase/Utils/StructuralMap.cs b/DeepDrftContent/FileDatabase/Utils/StructuralMap.cs index 67ab43f..3e0ec90 100644 --- a/DeepDrftContent/FileDatabase/Utils/StructuralMap.cs +++ b/DeepDrftContent/FileDatabase/Utils/StructuralMap.cs @@ -6,25 +6,43 @@ namespace DeepDrftContent.FileDatabase.Utils; /// /// A map implementation that uses structural equality for keys by serializing them to JSON. /// This provides the same behavior as the TypeScript StructuralMap. +/// Optimized with caching to avoid repeated serialization. /// /// The key type /// The value type -public class StructuralMap : IEnumerable> +public class StructuralMap : IEnumerable> where TKey : notnull { private readonly Dictionary> _innerMap = new(); + private readonly Dictionary _keyStringCache = new(); /// /// Converts a key to its string representation for structural comparison + /// Uses caching to avoid expensive serialization on repeated lookups /// private string GetKeyString(TKey key) { - return key switch + if (key == null) return "null"; + + // For reference types, use cache to avoid repeated serialization + if (!typeof(TKey).IsValueType && _keyStringCache.TryGetValue(key, out var cached)) + { + return cached; + } + + var keyString = key switch { - null => "null", string s => s, int or long or float or double or decimal => key.ToString()!, _ => JsonSerializer.Serialize(key) }; + + // Cache for reference types only (value types are cheap to convert) + if (!typeof(TKey).IsValueType) + { + _keyStringCache[key] = keyString; + } + + return keyString; } /// diff --git a/DeepDrftContent/FileDatabase/Utils/StructuralSet.cs b/DeepDrftContent/FileDatabase/Utils/StructuralSet.cs index 4bdc000..36a8751 100644 --- a/DeepDrftContent/FileDatabase/Utils/StructuralSet.cs +++ b/DeepDrftContent/FileDatabase/Utils/StructuralSet.cs @@ -6,24 +6,42 @@ namespace DeepDrftContent.FileDatabase.Utils; /// /// A set implementation that uses structural equality for values by serializing them to JSON. /// This provides the same behavior as the TypeScript StructuralSet. +/// Optimized with caching to avoid repeated serialization. /// /// The value type -public class StructuralSet : IEnumerable +public class StructuralSet : IEnumerable where T : notnull { private readonly Dictionary _innerMap = new(); + private readonly Dictionary _valueStringCache = new(); /// /// Converts a value to its string representation for structural comparison + /// Uses caching to avoid expensive serialization on repeated lookups /// private string GetValueString(T value) { - return value switch + if (value == null) return "null"; + + // For reference types, use cache to avoid repeated serialization + if (!typeof(T).IsValueType && _valueStringCache.TryGetValue(value, out var cached)) + { + return cached; + } + + var valueString = value switch { - null => "null", string s => s, int or long or float or double or decimal => value.ToString()!, _ => JsonSerializer.Serialize(value) }; + + // Cache for reference types only (value types are cheap to convert) + if (!typeof(T).IsValueType) + { + _valueStringCache[value] = valueString; + } + + return valueString; } /// diff --git a/DeepDrftContent/Startup.cs b/DeepDrftContent/Startup.cs index 2635f11..17dbc8f 100644 --- a/DeepDrftContent/Startup.cs +++ b/DeepDrftContent/Startup.cs @@ -1,18 +1,31 @@ +using DeepDrftContent.FileDatabase.Models; +using DeepDrftContent.FileDatabase.Services; using DeepDrftContent.Models; namespace DeepDrftContent { public static class Startup { - public static void ConfigureDomainServices(WebApplicationBuilder builder) + public static async Task ConfigureDomainServices(WebApplicationBuilder builder) { // File Database builder.Configuration.AddJsonFile("environment/filedatabase.json", optional: false, reloadOnChange: true); var fileDatabaseSettings = builder.Configuration.GetSection(nameof(FileDatabaseSettings)).Get(); if (fileDatabaseSettings is null) { throw new Exception("File database settings are not configured"); } - builder.Services.AddSingleton( - FileDatabase.Services.FileDatabase.FromAsync( - fileDatabaseSettings.VaultPath)); + + var fileDatabase = await FileDatabase.Services.FileDatabase.FromAsync(fileDatabaseSettings.VaultPath); + if (fileDatabase is null) { throw new Exception("Unable to initialize file database"); } + builder.Services.AddSingleton(fileDatabase); + await InitializeTrackVault(fileDatabase); + } + + private static async Task InitializeTrackVault(FileDatabase.Services.FileDatabase fileDatabase) + { + var vaultKey = new EntryKey("tracks", MediaVaultType.Audio); + if (!fileDatabase.HasVault(vaultKey)) + { + await fileDatabase.CreateVaultAsync(vaultKey); + } } } } \ No newline at end of file diff --git a/DeepDrftTests/IndexSystemTests.cs b/DeepDrftTests/IndexSystemTests.cs new file mode 100644 index 0000000..800095a --- /dev/null +++ b/DeepDrftTests/IndexSystemTests.cs @@ -0,0 +1,605 @@ +using DeepDrftContent.FileDatabase.Abstractions; +using DeepDrftContent.FileDatabase.Models; +using DeepDrftContent.FileDatabase.Services; +using DeepDrftContent.FileDatabase.Utils; + +namespace DeepDrftTests; + +/// +/// SOLID, DRY tests for IndexSystem components +/// Follows Single Responsibility: Each test class tests one concern +/// Follows DRY: Shared setup and helper methods +/// +[TestFixture] +public class IndexSystemTests +{ + /// + /// Base test class for index-related tests - DRY principle + /// + public abstract class IndexTestBase + { + protected string TestDirectory { get; private set; } = null!; + protected string IndexPath => Path.Combine(TestDirectory, "index"); + + [SetUp] + public virtual void SetUp() + { + TestDirectory = Path.Combine(Path.GetTempPath(), "DeepDrftTests", "IndexSystem", Guid.NewGuid().ToString()); + Directory.CreateDirectory(TestDirectory); + } + + [TearDown] + public virtual void TearDown() + { + if (Directory.Exists(TestDirectory)) + { + try { Directory.Delete(TestDirectory, true); } catch { /* Ignore cleanup errors */ } + } + } + + /// + /// Helper method to create test entry keys - DRY principle + /// + protected static EntryKey CreateTestEntryKey(string key, MediaVaultType type = MediaVaultType.Image) + => new(key, type); + + /// + /// Helper method to create test metadata - DRY principle + /// + protected static MetaData CreateTestMetaData(string key, string extension = ".png") + => new(key, extension); + } + + /// + /// Tests for IndexFactoryService - Single Responsibility Principle + /// + [TestFixture] + public class IndexFactoryServiceTests : IndexTestBase + { + private IndexFactoryService _factory = null!; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + _factory = new IndexFactoryService(); + } + + [Test] + public async Task CreateIndexAsync_DirectoryType_CreatesDirectoryIndex() + { + // Act + var index = await _factory.CreateIndexAsync(IndexType.Directory, TestDirectory); + + // Assert + Assert.That(index, Is.Not.Null, "Index should be created"); + Assert.That(index, Is.TypeOf(), "Should create DirectoryIndex"); + Assert.That(File.Exists(IndexPath), Is.True, "Index file should be created"); + } + + [Test] + public async Task CreateIndexAsync_VaultType_CreatesVaultIndex() + { + // Act + var index = await _factory.CreateIndexAsync(IndexType.Vault, TestDirectory); + + // Assert + Assert.That(index, Is.Not.Null, "Index should be created"); + Assert.That(index, Is.TypeOf(), "Should create VaultIndex"); + Assert.That(File.Exists(IndexPath), Is.True, "Index file should be created"); + } + + [Test] + public void CreateIndexAsync_InvalidType_ThrowsArgumentException() + { + // Arrange + var invalidType = (IndexType)999; + + // Act & Assert + Assert.ThrowsAsync(async () => + await _factory.CreateIndexAsync(invalidType, TestDirectory), + "Should throw for invalid index type"); + } + + [Test] + public async Task LoadIndexAsync_ExistingDirectoryIndex_LoadsSuccessfully() + { + // Arrange - Create an index first + await _factory.CreateIndexAsync(IndexType.Directory, TestDirectory); + + // Act + var loadedIndex = await _factory.LoadIndexAsync(IndexType.Directory, TestDirectory); + + // Assert + Assert.That(loadedIndex, Is.Not.Null, "Index should be loaded"); + Assert.That(loadedIndex, Is.TypeOf(), "Should load DirectoryIndex"); + } + + [Test] + public async Task LoadIndexAsync_ExistingVaultIndex_LoadsSuccessfully() + { + // Arrange - Create an index first + await _factory.CreateIndexAsync(IndexType.Vault, TestDirectory); + + // Act + var loadedIndex = await _factory.LoadIndexAsync(IndexType.Vault, TestDirectory); + + // Assert + Assert.That(loadedIndex, Is.Not.Null, "Index should be loaded"); + Assert.That(loadedIndex, Is.TypeOf(), "Should load VaultIndex"); + } + + [Test] + public void LoadIndexAsync_NonExistentIndex_ThrowsException() + { + // Act & Assert + Assert.ThrowsAsync(async () => + await _factory.LoadIndexAsync(IndexType.Directory, TestDirectory), + "Should throw when index file doesn't exist"); + } + + [Test] + public async Task LoadOrCreateIndexAsync_ExistingIndex_LoadsExisting() + { + // Arrange - Create an index with data + var originalIndex = await _factory.CreateIndexAsync(IndexType.Directory, TestDirectory); + Assert.That(originalIndex, Is.TypeOf(), "Should create DirectoryIndex"); + + var directoryIndex = (DirectoryIndex)originalIndex!; + var testKey = CreateTestEntryKey("test-entry"); + directoryIndex.PutEntry(testKey); + + // Save the modified index + var indexData = _factory.CreateIndexData(IndexType.Directory, directoryIndex); + await FileUtils.PutObjectAsync(IndexPath, indexData); + + // Act + var loadedIndex = await _factory.LoadOrCreateIndexAsync(IndexType.Directory, TestDirectory); + + // Assert + Assert.That(loadedIndex, Is.Not.Null, "Index should be loaded"); + Assert.That(loadedIndex, Is.TypeOf(), "Should load DirectoryIndex"); + Assert.That(loadedIndex, Is.InstanceOf(), "Should implement IEntryQueryable"); + + var queryableIndex = (IEntryQueryable)loadedIndex!; + Assert.That(queryableIndex.GetEntriesSize(), Is.EqualTo(1), "Should preserve existing entries"); + } + + [Test] + public async Task LoadOrCreateIndexAsync_NonExistentIndex_CreatesNew() + { + // Act + var index = await _factory.LoadOrCreateIndexAsync(IndexType.Directory, TestDirectory); + + // Assert + Assert.That(index, Is.Not.Null, "Index should be created"); + Assert.That(index, Is.TypeOf(), "Should create DirectoryIndex"); + Assert.That(index, Is.InstanceOf(), "Should implement IEntryQueryable"); + + var queryableIndex = (IEntryQueryable)index!; + Assert.That(queryableIndex.GetEntriesSize(), Is.EqualTo(0), "New index should be empty"); + } + + [Test] + public void CreateIndexData_DirectoryIndex_CreatesDirectoryIndexData() + { + // Arrange + var directoryIndex = new DirectoryIndex(new DirectoryIndexData("test")); + var testKey = CreateTestEntryKey("test-entry"); + directoryIndex.PutEntry(testKey); + + // Act + var indexData = _factory.CreateIndexData(IndexType.Directory, directoryIndex); + + // Assert + Assert.That(indexData, Is.TypeOf(), "Should create DirectoryIndexData"); + var typedData = (DirectoryIndexData)indexData; + Assert.That(typedData.IndexKey, Is.EqualTo("test"), "Should preserve index key"); + } + + [Test] + public void CreateIndexData_VaultIndex_CreatesVaultIndexData() + { + // Arrange + var vaultIndex = new VaultIndex(new VaultIndexData("test")); + var testKey = CreateTestEntryKey("test-entry"); + var testMetaData = CreateTestMetaData("test-entry"); + vaultIndex.PutEntry(testKey, testMetaData); + + // Act + var indexData = _factory.CreateIndexData(IndexType.Vault, vaultIndex); + + // Assert + Assert.That(indexData, Is.TypeOf(), "Should create VaultIndexData"); + var typedData = (VaultIndexData)indexData; + Assert.That(typedData.IndexKey, Is.EqualTo("test"), "Should preserve index key"); + } + + [Test] + public void CreateIndexFromData_DirectoryIndexData_CreatesDirectoryIndex() + { + // Arrange + var indexData = new DirectoryIndexData("test"); + + // Act + var index = _factory.CreateIndexFromData(IndexType.Directory, indexData); + + // Assert + Assert.That(index, Is.TypeOf(), "Should create DirectoryIndex"); + var queryableIndex = (IEntryQueryable)index; + Assert.That(queryableIndex.GetEntriesSize(), Is.EqualTo(0), "Should be empty initially"); + } + + [Test] + public void CreateIndexFromData_VaultIndexData_CreatesVaultIndex() + { + // Arrange + var indexData = new VaultIndexData("test"); + + // Act + var index = _factory.CreateIndexFromData(IndexType.Vault, indexData); + + // Assert + Assert.That(index, Is.TypeOf(), "Should create VaultIndex"); + var queryableIndex = (IEntryQueryable)index; + Assert.That(queryableIndex.GetEntriesSize(), Is.EqualTo(0), "Should be empty initially"); + } + } + + /// + /// Tests for DirectoryIndex - Single Responsibility Principle + /// + [TestFixture] + public class DirectoryIndexTests : IndexTestBase + { + private DirectoryIndex _directoryIndex = null!; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + _directoryIndex = new DirectoryIndex(new DirectoryIndexData("test-directory")); + } + + [Test] + public void DirectoryIndex_InitialState_IsEmpty() + { + // Assert + Assert.That(_directoryIndex.GetEntriesSize(), Is.EqualTo(0), "Should be empty initially"); + Assert.That(_directoryIndex.GetEntries(), Is.Empty, "Should have no entries"); + } + + [Test] + public void PutEntry_NewEntry_AddsSuccessfully() + { + // Arrange + var testKey = CreateTestEntryKey("new-entry"); + + // Act + _directoryIndex.PutEntry(testKey); + + // Assert + Assert.That(_directoryIndex.GetEntriesSize(), Is.EqualTo(1), "Should have one entry"); + Assert.That(_directoryIndex.HasEntry(testKey), Is.True, "Should contain the entry"); + Assert.That(_directoryIndex.GetEntries(), Contains.Item(testKey), "Should include entry in collection"); + } + + [Test] + public void PutEntry_DuplicateEntry_DoesNotDuplicate() + { + // Arrange + var testKey = CreateTestEntryKey("duplicate-entry"); + _directoryIndex.PutEntry(testKey); + + // Act + _directoryIndex.PutEntry(testKey); + + // Assert + Assert.That(_directoryIndex.GetEntriesSize(), Is.EqualTo(1), "Should still have only one entry"); + } + + [Test] + public void PutEntry_MultipleEntries_AddsAll() + { + // Arrange + var keys = new[] + { + CreateTestEntryKey("entry1"), + CreateTestEntryKey("entry2"), + CreateTestEntryKey("entry3") + }; + + // Act + foreach (var key in keys) + { + _directoryIndex.PutEntry(key); + } + + // Assert + Assert.That(_directoryIndex.GetEntriesSize(), Is.EqualTo(3), "Should have three entries"); + foreach (var key in keys) + { + Assert.That(_directoryIndex.HasEntry(key), Is.True, $"Should contain {key}"); + } + } + + [Test] + public void HasEntry_ExistingEntry_ReturnsTrue() + { + // Arrange + var testKey = CreateTestEntryKey("existing-entry"); + _directoryIndex.PutEntry(testKey); + + // Act & Assert + Assert.That(_directoryIndex.HasEntry(testKey), Is.True, "Should find existing entry"); + } + + [Test] + public void HasEntry_NonExistentEntry_ReturnsFalse() + { + // Arrange + var testKey = CreateTestEntryKey("non-existent"); + + // Act & Assert + Assert.That(_directoryIndex.HasEntry(testKey), Is.False, "Should not find non-existent entry"); + } + + [Test] + public void GetEntries_WithMultipleEntries_ReturnsAllEntries() + { + // Arrange + var keys = new[] + { + CreateTestEntryKey("entry1"), + CreateTestEntryKey("entry2"), + CreateTestEntryKey("entry3") + }; + + foreach (var key in keys) + { + _directoryIndex.PutEntry(key); + } + + // Act + var entries = _directoryIndex.GetEntries(); + + // Assert + Assert.That(entries.Count, Is.EqualTo(3), "Should return all entries"); + foreach (var key in keys) + { + Assert.That(entries, Contains.Item(key), $"Should contain {key}"); + } + } + } + + /// + /// Tests for VaultIndex - Single Responsibility Principle + /// + [TestFixture] + public class VaultIndexTests : IndexTestBase + { + private VaultIndex _vaultIndex = null!; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + _vaultIndex = new VaultIndex(new VaultIndexData("test-vault")); + } + + [Test] + public void VaultIndex_InitialState_IsEmpty() + { + // Assert + Assert.That(_vaultIndex.GetEntriesSize(), Is.EqualTo(0), "Should be empty initially"); + Assert.That(_vaultIndex.GetEntries(), Is.Empty, "Should have no entries"); + } + + [Test] + public void PutEntry_NewEntryWithMetadata_AddsSuccessfully() + { + // Arrange + var testKey = CreateTestEntryKey("new-entry"); + var testMetaData = CreateTestMetaData("new-entry"); + + // Act + _vaultIndex.PutEntry(testKey, testMetaData); + + // Assert + Assert.That(_vaultIndex.GetEntriesSize(), Is.EqualTo(1), "Should have one entry"); + Assert.That(_vaultIndex.HasEntry(testKey), Is.True, "Should contain the entry"); + Assert.That(_vaultIndex.GetEntry(testKey), Is.EqualTo(testMetaData), "Should return correct metadata"); + } + + [Test] + public void PutEntry_DuplicateEntry_UpdatesMetadata() + { + // Arrange + var testKey = CreateTestEntryKey("duplicate-entry"); + var originalMetaData = CreateTestMetaData("original"); + var updatedMetaData = CreateTestMetaData("updated"); + + _vaultIndex.PutEntry(testKey, originalMetaData); + + // Act + _vaultIndex.PutEntry(testKey, updatedMetaData); + + // Assert + Assert.That(_vaultIndex.GetEntriesSize(), Is.EqualTo(1), "Should still have only one entry"); + Assert.That(_vaultIndex.GetEntry(testKey), Is.EqualTo(updatedMetaData), "Should have updated metadata"); + } + + [Test] + public void GetEntry_ExistingEntry_ReturnsMetadata() + { + // Arrange + var testKey = CreateTestEntryKey("existing-entry"); + var testMetaData = CreateTestMetaData("existing-entry"); + _vaultIndex.PutEntry(testKey, testMetaData); + + // Act + var retrievedMetaData = _vaultIndex.GetEntry(testKey); + + // Assert + Assert.That(retrievedMetaData, Is.EqualTo(testMetaData), "Should return correct metadata"); + } + + [Test] + public void GetEntry_NonExistentEntry_ReturnsNull() + { + // Arrange + var testKey = CreateTestEntryKey("non-existent"); + + // Act + var retrievedMetaData = _vaultIndex.GetEntry(testKey); + + // Assert + Assert.That(retrievedMetaData, Is.Null, "Should return null for non-existent entry"); + } + + [Test] + public void PutEntry_MultipleEntriesWithDifferentMetadata_AddsAll() + { + // Arrange + var entries = new[] + { + (CreateTestEntryKey("entry1"), CreateTestMetaData("entry1", ".png")), + (CreateTestEntryKey("entry2"), CreateTestMetaData("entry2", ".jpg")), + (CreateTestEntryKey("entry3"), CreateTestMetaData("entry3", ".gif")) + }; + + // Act + foreach (var (key, metaData) in entries) + { + _vaultIndex.PutEntry(key, metaData); + } + + // Assert + Assert.That(_vaultIndex.GetEntriesSize(), Is.EqualTo(3), "Should have three entries"); + foreach (var (key, metaData) in entries) + { + Assert.That(_vaultIndex.HasEntry(key), Is.True, $"Should contain {key}"); + Assert.That(_vaultIndex.GetEntry(key), Is.EqualTo(metaData), $"Should have correct metadata for {key}"); + } + } + } + + /// + /// Tests for IndexFactory - Single Responsibility Principle + /// + [TestFixture] + public class IndexFactoryTests : IndexTestBase + { + [Test] + public async Task IndexFactory_DirectoryType_BuildsDirectoryIndex() + { + // Arrange + var factory = new IndexFactory(TestDirectory, IndexType.Directory); + + // Act + var index = await factory.BuildIndexAsync(); + + // Assert + Assert.That(index, Is.Not.Null, "Index should be built"); + Assert.That(index, Is.TypeOf(), "Should build DirectoryIndex"); + Assert.That(File.Exists(IndexPath), Is.True, "Index file should be created"); + } + + [Test] + public async Task IndexFactory_VaultType_BuildsVaultIndex() + { + // Arrange + var factory = new IndexFactory(TestDirectory, IndexType.Vault); + + // Act + var index = await factory.BuildIndexAsync(); + + // Assert + Assert.That(index, Is.Not.Null, "Index should be built"); + Assert.That(index, Is.TypeOf(), "Should build VaultIndex"); + Assert.That(File.Exists(IndexPath), Is.True, "Index file should be created"); + } + + [Test] + public async Task IndexFactory_ExistingIndex_LoadsExistingData() + { + // Arrange - Create index with data + var factory = new IndexFactory(TestDirectory, IndexType.Directory); + var originalIndex = await factory.BuildIndexAsync(); + var directoryIndex = (DirectoryIndex)originalIndex!; + var testKey = CreateTestEntryKey("persisted-entry"); + directoryIndex.PutEntry(testKey); + + // Save the index manually + var factoryService = new IndexFactoryService(); + var indexData = factoryService.CreateIndexData(IndexType.Directory, directoryIndex); + await FileUtils.PutObjectAsync(IndexPath, indexData); + + // Act - Create new factory and build + var newFactory = new IndexFactory(TestDirectory, IndexType.Directory); + var loadedIndex = await newFactory.BuildIndexAsync(); + + // Assert + Assert.That(loadedIndex, Is.Not.Null, "Index should be loaded"); + Assert.That(loadedIndex, Is.InstanceOf(), "Should implement IEntryQueryable"); + + var queryableIndex = (IEntryQueryable)loadedIndex!; + Assert.That(queryableIndex.GetEntriesSize(), Is.EqualTo(1), "Should load existing entry"); + Assert.That(queryableIndex.HasEntry(testKey), Is.True, "Should contain persisted entry"); + } + } + + /// + /// Integration tests for IndexDirectory classes - Open/Closed Principle + /// + [TestFixture] + public class IndexDirectoryIntegrationTests : IndexTestBase + { + [Test] + public async Task DirectoryIndexDirectory_AddToIndex_PersistsChanges() + { + // This test would require access to protected methods, so we'll test through the FileDatabase instead + // which properly encapsulates the DirectoryIndexDirectory functionality + + // Arrange + var database = await FileDatabase.FromAsync(TestDirectory); + var testVaultKey = CreateTestEntryKey("test-vault"); + + // Act - This internally uses DirectoryIndexDirectory.AddToIndexAsync + await database!.CreateVaultAsync(testVaultKey); + + // Assert + Assert.That(database.HasIndexEntry(testVaultKey), Is.True, "Should contain added entry"); + Assert.That(database.GetIndexSize(), Is.EqualTo(1), "Should have one entry"); + Assert.That(File.Exists(IndexPath), Is.True, "Should persist to file"); + + // Verify persistence by reloading database + var reloadedDatabase = await FileDatabase.FromAsync(TestDirectory); + Assert.That(reloadedDatabase!.HasIndexEntry(testVaultKey), Is.True, "Should persist entry across restarts"); + } + + [Test] + public async Task VaultIndexDirectory_AddToIndex_PersistsChanges() + { + // This test would require access to protected methods, so we'll test through MediaVault instead + // which properly encapsulates the VaultIndexDirectory functionality + + // Arrange + var vault = await ImageVault.FromAsync(TestDirectory); + var testKey = CreateTestEntryKey("test-entry"); + var testImage = TestData.CreateTestImageBinary(1.0); + + // Act - This internally uses VaultIndexDirectory.AddToIndexAsync + await vault!.AddEntryAsync(MediaVaultType.Image, testKey, testImage); + + // Assert + Assert.That(vault.HasIndexEntry(testKey), Is.True, "Should contain added entry"); + Assert.That(vault.GetIndexSize(), Is.EqualTo(1), "Should have one entry"); + Assert.That(File.Exists(IndexPath), Is.True, "Should persist to file"); + + // Verify persistence by reloading vault + var reloadedVault = await ImageVault.FromAsync(TestDirectory); + Assert.That(reloadedVault!.HasIndexEntry(testKey), Is.True, "Should persist entry across restarts"); + } + } +} diff --git a/DeepDrftTests/MediaVaultFactoryTests.cs b/DeepDrftTests/MediaVaultFactoryTests.cs new file mode 100644 index 0000000..f720fe6 --- /dev/null +++ b/DeepDrftTests/MediaVaultFactoryTests.cs @@ -0,0 +1,333 @@ +using DeepDrftContent.FileDatabase.Models; +using DeepDrftContent.FileDatabase.Services; + +namespace DeepDrftTests; + +/// +/// SOLID, DRY tests for MediaVaultFactory +/// Follows Single Responsibility: Each test focuses on one factory behavior +/// Follows Open/Closed: Tests extensibility through MediaVaultType enum +/// +[TestFixture] +public class MediaVaultFactoryTests +{ + /// + /// Base class for MediaVaultFactory tests - DRY principle + /// + public abstract class MediaVaultFactoryTestBase + { + protected string TestDirectory { get; private set; } = null!; + + [SetUp] + public virtual void SetUp() + { + TestDirectory = Path.Combine(Path.GetTempPath(), "DeepDrftTests", "MediaVaultFactory", Guid.NewGuid().ToString()); + Directory.CreateDirectory(TestDirectory); + } + + [TearDown] + public virtual void TearDown() + { + if (Directory.Exists(TestDirectory)) + { + try { Directory.Delete(TestDirectory, true); } catch { /* Ignore cleanup errors */ } + } + } + + /// + /// Helper method to verify vault creation - DRY principle + /// + protected static void AssertVaultCreated(MediaVault? vault, string testContext) where T : MediaVault + { + Assert.That(vault, Is.Not.Null, $"Vault should be created for {testContext}"); + Assert.That(vault, Is.TypeOf(), $"Should create correct vault type for {testContext}"); + } + } + + /// + /// Tests for basic factory functionality - Single Responsibility + /// + [TestFixture] + public class BasicFactoryTests : MediaVaultFactoryTestBase + { + [Test] + public async Task From_ImageVaultType_CreatesImageVault() + { + // Act + var vault = await MediaVaultFactory.From(TestDirectory, MediaVaultType.Image); + + // Assert + AssertVaultCreated(vault, "Image vault creation"); + } + + [Test] + public async Task From_AudioVaultType_CreatesAudioVault() + { + // Act + var vault = await MediaVaultFactory.From(TestDirectory, MediaVaultType.Audio); + + // Assert + AssertVaultCreated(vault, "Audio vault creation"); + } + + [Test] + public async Task From_MediaVaultType_ReturnsNull() + { + // Note: MediaVaultType.Media doesn't have a concrete vault implementation + // This tests the factory's handling of unsupported types + + // Act + var vault = await MediaVaultFactory.From(TestDirectory, MediaVaultType.Media); + + // Assert + Assert.That(vault, Is.Null, "Should return null for unsupported Media vault type"); + } + + [Test] + public async Task From_InvalidVaultType_ReturnsNull() + { + // Arrange + var invalidType = (MediaVaultType)999; + + // Act + var vault = await MediaVaultFactory.From(TestDirectory, invalidType); + + // Assert + Assert.That(vault, Is.Null, "Should return null for invalid vault type"); + } + } + + /// + /// Tests for factory behavior with different directory states - Interface Segregation + /// + [TestFixture] + public class DirectoryStateTests : MediaVaultFactoryTestBase + { + [Test] + public async Task From_NonExistentDirectory_CreatesDirectoryAndVault() + { + // Arrange + var nonExistentPath = Path.Combine(TestDirectory, "non-existent"); + Assert.That(Directory.Exists(nonExistentPath), Is.False, "Directory should not exist initially"); + + // Act + var vault = await MediaVaultFactory.From(nonExistentPath, MediaVaultType.Image); + + // Assert + AssertVaultCreated(vault, "Non-existent directory"); + Assert.That(Directory.Exists(nonExistentPath), Is.True, "Directory should be created"); + } + + [Test] + public async Task From_ExistingEmptyDirectory_CreatesVault() + { + // Arrange - Directory already exists but is empty + Assert.That(Directory.Exists(TestDirectory), Is.True, "Directory should exist"); + Assert.That(Directory.GetFileSystemEntries(TestDirectory), Is.Empty, "Directory should be empty"); + + // Act + var vault = await MediaVaultFactory.From(TestDirectory, MediaVaultType.Image); + + // Assert + AssertVaultCreated(vault, "Existing empty directory"); + } + + [Test] + public async Task From_ExistingDirectoryWithIndex_LoadsExistingVault() + { + // Arrange - Create a vault first to establish an index + var originalVault = await MediaVaultFactory.From(TestDirectory, MediaVaultType.Image); + Assert.That(originalVault, Is.Not.Null, "Original vault should be created"); + + // Act - Create another vault from the same directory + var reloadedVault = await MediaVaultFactory.From(TestDirectory, MediaVaultType.Image); + + // Assert + AssertVaultCreated(reloadedVault, "Existing directory with index"); + + // Verify both vaults reference the same underlying directory + Assert.That(reloadedVault!.RootPath, Is.EqualTo(originalVault!.RootPath), + "Both vaults should reference the same directory"); + } + } + + /// + /// Tests for factory error handling - Dependency Inversion Principle + /// + [TestFixture] + public class ErrorHandlingTests : MediaVaultFactoryTestBase + { + [Test] + public async Task From_InvalidPath_HandlesGracefully() + { + // Arrange - Use invalid path characters + var invalidPath = Path.Combine(TestDirectory, "invalid<>path"); + + // Act & Assert - On Windows, this will throw IOException, which is expected behavior + // The factory doesn't need to handle every possible OS-level path error + try + { + await MediaVaultFactory.From(invalidPath, MediaVaultType.Image); + // If we get here without exception, that's also fine + Assert.Pass("Factory handled invalid path without throwing"); + } + catch (IOException) + { + // This is expected behavior on Windows + Assert.Pass("Factory correctly propagated OS-level path error"); + } + catch (Exception ex) + { + Assert.Fail($"Factory threw unexpected exception type: {ex.GetType().Name}"); + } + } + + [Test] + public async Task From_ReadOnlyDirectory_HandlesGracefully() + { + // This test is platform-specific and may behave differently on different OS + // On Windows, this test may not be as effective as on Unix systems + + // Act & Assert - Should not throw exceptions + Assert.DoesNotThrowAsync(async () => + { + await MediaVaultFactory.From(TestDirectory, MediaVaultType.Image); + }, "Factory should handle permission issues gracefully"); + } + + [Test] + public async Task From_VeryLongPath_HandlesGracefully() + { + // Arrange - Create a very long path (but within reasonable limits) + var longDirectoryName = new string('a', 100); + var longPath = Path.Combine(TestDirectory, longDirectoryName); + + // Act & Assert - Should not throw + Assert.DoesNotThrowAsync(async () => + { + await MediaVaultFactory.From(longPath, MediaVaultType.Image); + }, "Factory should handle long paths gracefully"); + } + } + + /// + /// Tests for factory consistency and idempotency - Liskov Substitution Principle + /// + [TestFixture] + public class ConsistencyTests : MediaVaultFactoryTestBase + { + [Test] + public async Task From_SameParametersMultipleCalls_ReturnsConsistentResults() + { + // Act + var vault1 = await MediaVaultFactory.From(TestDirectory, MediaVaultType.Image); + var vault2 = await MediaVaultFactory.From(TestDirectory, MediaVaultType.Image); + + // Assert + Assert.That(vault1, Is.Not.Null, "First vault should be created"); + Assert.That(vault2, Is.Not.Null, "Second vault should be created"); + Assert.That(vault1!.GetType(), Is.EqualTo(vault2!.GetType()), "Both vaults should be same type"); + Assert.That(vault1.RootPath, Is.EqualTo(vault2.RootPath), "Both vaults should have same root path"); + } + + [Test] + public async Task From_DifferentVaultTypesInSameDirectory_CreatesAppropriateTypes() + { + // Act + var imageVault = await MediaVaultFactory.From(TestDirectory, MediaVaultType.Image); + var audioVault = await MediaVaultFactory.From(TestDirectory, MediaVaultType.Audio); + + // Assert + AssertVaultCreated(imageVault, "Image vault in shared directory"); + AssertVaultCreated(audioVault, "Audio vault in shared directory"); + + // Both should reference the same directory but be different types + Assert.That(imageVault!.RootPath, Is.EqualTo(audioVault!.RootPath), + "Both vaults should reference the same directory"); + Assert.That(imageVault.GetType(), Is.Not.EqualTo(audioVault.GetType()), + "Vaults should be different types"); + } + + [Test] + public async Task From_ConcurrentCalls_HandlesGracefully() + { + // Arrange + var tasks = new List>(); + const int concurrentCalls = 5; + + // Act - Make multiple concurrent calls + for (int i = 0; i < concurrentCalls; i++) + { + var subdirectory = Path.Combine(TestDirectory, $"concurrent-{i}"); + tasks.Add(MediaVaultFactory.From(subdirectory, MediaVaultType.Image)); + } + + var results = await Task.WhenAll(tasks); + + // Assert + Assert.That(results.Length, Is.EqualTo(concurrentCalls), "Should complete all concurrent calls"); + + foreach (var (result, index) in results.Select((r, i) => (r, i))) + { + AssertVaultCreated(result, $"Concurrent call {index}"); + } + } + } + + /// + /// Tests for factory integration with underlying registry - Dependency Inversion + /// + [TestFixture] + public class RegistryIntegrationTests : MediaVaultFactoryTestBase + { + [Test] + public async Task From_AllSupportedVaultTypes_CreatesCorrectTypes() + { + // Test all currently supported vault types + var supportedTypes = new[] + { + (MediaVaultType.Image, typeof(ImageVault)), + (MediaVaultType.Audio, typeof(AudioVault)) + }; + + foreach (var (vaultType, expectedType) in supportedTypes) + { + // Arrange + var subdirectory = Path.Combine(TestDirectory, vaultType.ToString().ToLower()); + + // Act + var vault = await MediaVaultFactory.From(subdirectory, vaultType); + + // Assert + Assert.That(vault, Is.Not.Null, $"Should create vault for {vaultType}"); + Assert.That(vault!.GetType(), Is.EqualTo(expectedType), $"Should create {expectedType.Name} for {vaultType}"); + } + } + + [Test] + public async Task From_FactoryUsesUnderlyingRegistry_ConsistentWithRegistryBehavior() + { + // This test verifies that the factory delegates properly to the registry + // and behaves consistently with direct registry usage + + // Arrange + var registry = new SimpleMediaTypeRegistry(); + + // Act + var factoryVault = await MediaVaultFactory.From(TestDirectory, MediaVaultType.Image); + var registryVault = await registry.CreateVaultAsync(MediaVaultType.Image, TestDirectory); + + // Assert + if (factoryVault != null && registryVault != null) + { + Assert.That(factoryVault.GetType(), Is.EqualTo(registryVault.GetType()), + "Factory and registry should create same vault types"); + } + else + { + Assert.That(factoryVault, Is.EqualTo(registryVault), + "Both factory and registry should return same null/non-null result"); + } + } + } +} diff --git a/DeepDrftTests/MediaVaultTests.cs b/DeepDrftTests/MediaVaultTests.cs new file mode 100644 index 0000000..137c345 --- /dev/null +++ b/DeepDrftTests/MediaVaultTests.cs @@ -0,0 +1,584 @@ +using DeepDrftContent.FileDatabase.Models; +using DeepDrftContent.FileDatabase.Services; +using DeepDrftContent.FileDatabase.Utils; + +namespace DeepDrftTests; + +/// +/// SOLID, DRY tests for MediaVault implementations +/// Follows Single Responsibility: Each test class tests one vault concern +/// Follows Liskov Substitution: Tests that all vault implementations behave consistently +/// Follows Dependency Inversion: Tests through abstractions where possible +/// +[TestFixture] +public class MediaVaultTests +{ + /// + /// Base class for MediaVault tests - DRY principle + /// + public abstract class MediaVaultTestBase + { + protected string TestDirectory { get; private set; } = null!; + protected string IndexPath => Path.Combine(TestDirectory, "index"); + + [SetUp] + public virtual void SetUp() + { + TestDirectory = Path.Combine(Path.GetTempPath(), "DeepDrftTests", "MediaVault", Guid.NewGuid().ToString()); + Directory.CreateDirectory(TestDirectory); + } + + [TearDown] + public virtual void TearDown() + { + if (Directory.Exists(TestDirectory)) + { + try { Directory.Delete(TestDirectory, true); } catch { /* Ignore cleanup errors */ } + } + } + + /// + /// Helper method to create test entry keys - DRY principle + /// + protected static EntryKey CreateTestEntryKey(string key, MediaVaultType type = MediaVaultType.Image) + => new(key, type); + + /// + /// Helper method to create test media files - DRY principle + /// + protected string CreateTestMediaFile(string fileName, byte[]? content = null) + { + content ??= TestData.TestPngBytes; + var filePath = Path.Combine(TestDirectory, fileName); + File.WriteAllBytes(filePath, content); + return filePath; + } + + /// + /// Helper method to verify media file exists and has correct content - DRY principle + /// + protected static void AssertMediaFileExists(string filePath, byte[] expectedContent) + { + Assert.That(File.Exists(filePath), Is.True, $"Media file should exist at {filePath}"); + var actualContent = File.ReadAllBytes(filePath); + Assert.That(actualContent, Is.EqualTo(expectedContent), "File content should match expected"); + } + } + + /// + /// Tests for ImageVault - Single Responsibility + /// + [TestFixture] + public class ImageVaultTests : MediaVaultTestBase + { + private ImageVault _imageVault = null!; + + [SetUp] + public async Task SetUpAsync() + { + base.SetUp(); // Call base synchronous setup first + _imageVault = await ImageVault.FromAsync(TestDirectory); + Assert.That(_imageVault, Is.Not.Null, "ImageVault should be created for tests"); + } + + [Test] + public async Task ImageVault_FromAsync_CreatesVaultWithIndex() + { + // Act + var vault = await ImageVault.FromAsync(TestDirectory); + + // Assert + Assert.That(vault, Is.Not.Null, "Should create ImageVault"); + Assert.That(vault!.RootPath, Is.EqualTo(TestDirectory), "Should use provided directory"); + Assert.That(File.Exists(IndexPath), Is.True, "Should create index file"); + } + + [Test] + public async Task ImageVault_FromAsync_NonExistentDirectory_CreatesDirectoryAndVault() + { + // Arrange + var newDirectory = Path.Combine(TestDirectory, "new-vault"); + Assert.That(Directory.Exists(newDirectory), Is.False, "Directory should not exist initially"); + + // Act + var vault = await ImageVault.FromAsync(newDirectory); + + // Assert + Assert.That(vault, Is.Not.Null, "Should create ImageVault"); + Assert.That(Directory.Exists(newDirectory), Is.True, "Should create directory"); + Assert.That(File.Exists(Path.Combine(newDirectory, "index")), Is.True, "Should create index"); + } + + [Test] + public async Task AddEntryAsync_ImageBinary_AddsToIndexAndCreatesFile() + { + // Arrange + var entryKey = CreateTestEntryKey("test-image"); + var imageBinary = TestData.CreateTestImageBinary(1.5); + + // Act + await _imageVault.AddEntryAsync(MediaVaultType.Image, entryKey, imageBinary); + + // Assert + Assert.That(_imageVault.HasIndexEntry(entryKey), Is.True, "Should add to index"); + + var expectedFilePath = Path.Combine(TestDirectory, "test-image.png"); + AssertMediaFileExists(expectedFilePath, imageBinary.Buffer); + } + + [Test] + public async Task AddEntryAsync_MultipleImages_AddsAllToIndexAndCreatesFiles() + { + // Arrange + var entries = new[] + { + (CreateTestEntryKey("image1"), TestData.CreateTestImageBinary(1.0)), + (CreateTestEntryKey("image2"), TestData.CreateTestImageBinary(1.5)), + (CreateTestEntryKey("image3"), TestData.CreateTestImageBinary(2.0)) + }; + + // Act + foreach (var (key, binary) in entries) + { + await _imageVault.AddEntryAsync(MediaVaultType.Image, key, binary); + } + + // Assert + Assert.That(_imageVault.GetIndexSize(), Is.EqualTo(3), "Should have three entries in index"); + + foreach (var (key, binary) in entries) + { + Assert.That(_imageVault.HasIndexEntry(key), Is.True, $"Should contain {key} in index"); + var expectedFilePath = Path.Combine(TestDirectory, $"{key.Key}.png"); + AssertMediaFileExists(expectedFilePath, binary.Buffer); + } + } + + [Test] + public async Task GetEntryAsync_ExistingImage_ReturnsImageBinary() + { + // Arrange + var entryKey = CreateTestEntryKey("existing-image"); + var originalImage = TestData.CreateTestImageBinary(1.77); + await _imageVault.AddEntryAsync(MediaVaultType.Image, entryKey, originalImage); + + // Act + var retrievedImage = await _imageVault.GetEntryAsync(MediaVaultType.Image, entryKey); + + // Assert + Assert.That(retrievedImage, Is.Not.Null, "Should retrieve image"); + Assert.That(retrievedImage!.Buffer, Is.EqualTo(originalImage.Buffer), "Buffer should match"); + Assert.That(retrievedImage.Extension, Is.EqualTo(originalImage.Extension), "Extension should match"); + Assert.That(retrievedImage.AspectRatio, Is.EqualTo(originalImage.AspectRatio), "Aspect ratio should match"); + } + + [Test] + public async Task GetEntryAsync_NonExistentImage_ReturnsNull() + { + // Arrange + var nonExistentKey = CreateTestEntryKey("non-existent"); + + // Act + var retrievedImage = await _imageVault.GetEntryAsync(MediaVaultType.Image, nonExistentKey); + + // Assert + Assert.That(retrievedImage, Is.Null, "Should return null for non-existent image"); + } + + [Test] + public async Task GetEntryAsync_IndexEntryExistsButFileDeleted_ReturnsNull() + { + // Arrange + var entryKey = CreateTestEntryKey("deleted-file"); + var imageBinary = TestData.CreateTestImageBinary(1.0); + await _imageVault.AddEntryAsync(MediaVaultType.Image, entryKey, imageBinary); + + // Delete the physical file but leave index entry + var filePath = Path.Combine(TestDirectory, "deleted-file.png"); + File.Delete(filePath); + + // Act + var retrievedImage = await _imageVault.GetEntryAsync(MediaVaultType.Image, entryKey); + + // Assert + Assert.That(retrievedImage, Is.Null, "Should return null when file is missing"); + } + + [Test] + public async Task AddEntryAsync_DuplicateKey_UpdatesExistingEntry() + { + // Arrange + var entryKey = CreateTestEntryKey("duplicate-key"); + var originalImage = TestData.CreateTestImageBinary(1.0); + var updatedImage = TestData.CreateTestImageBinary(2.0); + + // Act + await _imageVault.AddEntryAsync(MediaVaultType.Image, entryKey, originalImage); + await _imageVault.AddEntryAsync(MediaVaultType.Image, entryKey, updatedImage); + + // Assert + Assert.That(_imageVault.GetIndexSize(), Is.EqualTo(1), "Should still have only one entry"); + + var retrievedImage = await _imageVault.GetEntryAsync(MediaVaultType.Image, entryKey); + Assert.That(retrievedImage, Is.Not.Null, "Should retrieve updated image"); + Assert.That(retrievedImage!.AspectRatio, Is.EqualTo(2.0), "Should have updated aspect ratio"); + } + } + + /// + /// Tests for AudioVault - Single Responsibility (following same patterns as ImageVault) + /// + [TestFixture] + public class AudioVaultTests : MediaVaultTestBase + { + private AudioVault _audioVault = null!; + + [SetUp] + public async Task SetUpAsync() + { + base.SetUp(); // Call base synchronous setup first + _audioVault = await AudioVault.FromAsync(TestDirectory); + Assert.That(_audioVault, Is.Not.Null, "AudioVault should be created for tests"); + } + + [Test] + public async Task AudioVault_FromAsync_CreatesVaultWithIndex() + { + // Act + var vault = await AudioVault.FromAsync(TestDirectory); + + // Assert + Assert.That(vault, Is.Not.Null, "Should create AudioVault"); + Assert.That(vault!.RootPath, Is.EqualTo(TestDirectory), "Should use provided directory"); + Assert.That(File.Exists(IndexPath), Is.True, "Should create index file"); + } + + [Test] + public async Task AddEntryAsync_AudioBinary_AddsToIndexAndCreatesFile() + { + // Arrange + var entryKey = CreateTestEntryKey("test-audio", MediaVaultType.Audio); + var audioBinary = TestData.CreateTestAudioBinary(120.0, 320); + + // Act + await _audioVault.AddEntryAsync(MediaVaultType.Audio, entryKey, audioBinary); + + // Assert + Assert.That(_audioVault.HasIndexEntry(entryKey), Is.True, "Should add to index"); + + var expectedFilePath = Path.Combine(TestDirectory, "test-audio.mp3"); + AssertMediaFileExists(expectedFilePath, audioBinary.Buffer); + } + + [Test] + public async Task GetEntryAsync_ExistingAudio_ReturnsAudioBinary() + { + // Arrange + var entryKey = CreateTestEntryKey("existing-audio", MediaVaultType.Audio); + var originalAudio = TestData.CreateTestAudioBinary(180.5, 256); + await _audioVault.AddEntryAsync(MediaVaultType.Audio, entryKey, originalAudio); + + // Act + var retrievedAudio = await _audioVault.GetEntryAsync(MediaVaultType.Audio, entryKey); + + // Assert + Assert.That(retrievedAudio, Is.Not.Null, "Should retrieve audio"); + Assert.That(retrievedAudio!.Buffer, Is.EqualTo(originalAudio.Buffer), "Buffer should match"); + Assert.That(retrievedAudio.Extension, Is.EqualTo(originalAudio.Extension), "Extension should match"); + Assert.That(retrievedAudio.Duration, Is.EqualTo(originalAudio.Duration), "Duration should match"); + Assert.That(retrievedAudio.Bitrate, Is.EqualTo(originalAudio.Bitrate), "Bitrate should match"); + } + } + + /// + /// Tests for MediaVault abstract base class behavior - Liskov Substitution Principle + /// + [TestFixture] + public class MediaVaultBaseTests : MediaVaultTestBase + { + /// + /// Test implementation of MediaVault for testing abstract functionality + /// Uses ImageVault as concrete implementation to avoid creating test-specific vault + /// + private class TestMediaVaultWrapper + { + private readonly ImageVault _vault; + + public TestMediaVaultWrapper(ImageVault vault) + { + _vault = vault; + } + + public static async Task FromAsync(string rootPath) + { + var vault = await ImageVault.FromAsync(rootPath); + return vault != null ? new TestMediaVaultWrapper(vault) : null; + } + + // Expose protected methods for testing using reflection + public string GetMediaKey(string entryKey, string extension) + { + var method = typeof(MediaVault).GetMethod("GetMediaKey", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + return (string)method!.Invoke(_vault, new object[] { entryKey, extension })!; + } + + public string GetMediaPathFromEntryKey(string entryKey, string extension) + { + var method = typeof(MediaVault).GetMethod("GetMediaPathFromEntryKey", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + return (string)method!.Invoke(_vault, new object[] { entryKey, extension })!; + } + + public string GetMediaPathFromMediaKey(string mediaKey) + { + var method = typeof(MediaVault).GetMethod("GetMediaPathFromMediaKey", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + return (string)method!.Invoke(_vault, new object[] { mediaKey })!; + } + + public bool HasIndexEntry(EntryKey entryKey) => _vault.HasIndexEntry(entryKey); + public Task AddEntryAsync(MediaVaultType vaultType, EntryKey entryKey, object media) => + _vault.AddEntryAsync(vaultType, entryKey, media); + } + + [Test] + public async Task GetMediaKey_NormalKey_SanitizesCorrectly() + { + // Arrange + var vault = await TestMediaVaultWrapper.FromAsync(TestDirectory); + Assert.That(vault, Is.Not.Null, "Vault should be created"); + + // Act & Assert - Test various sanitization scenarios + Assert.That(vault!.GetMediaKey("normal-key", ".png"), Is.EqualTo("normal-key.png"), + "Normal key should pass through unchanged"); + + Assert.That(vault.GetMediaKey("key with spaces", ".jpg"), Is.EqualTo("key-with-spaces.jpg"), + "Spaces should be replaced with dashes"); + + Assert.That(vault.GetMediaKey("key@#$%special", ".gif"), Is.EqualTo("key----special.gif"), + "Special characters should be replaced with dashes"); + + Assert.That(vault.GetMediaKey("key123ABC", ".png"), Is.EqualTo("key123ABC.png"), + "Alphanumeric characters should be preserved"); + } + + [Test] + public async Task GetMediaPathFromEntryKey_ValidInputs_ReturnsCorrectPath() + { + // Arrange + var vault = await TestMediaVaultWrapper.FromAsync(TestDirectory); + Assert.That(vault, Is.Not.Null, "Vault should be created"); + + // Act + var path = vault!.GetMediaPathFromEntryKey("test-key", ".png"); + + // Assert + var expectedPath = Path.Combine(TestDirectory, "test-key.png"); + Assert.That(path, Is.EqualTo(expectedPath), "Should combine directory and sanitized filename"); + } + + [Test] + public async Task GetMediaPathFromMediaKey_ValidKey_ReturnsCorrectPath() + { + // Arrange + var vault = await TestMediaVaultWrapper.FromAsync(TestDirectory); + Assert.That(vault, Is.Not.Null, "Vault should be created"); + + // Act + var path = vault!.GetMediaPathFromMediaKey("media-file.png"); + + // Assert + var expectedPath = Path.Combine(TestDirectory, "media-file.png"); + Assert.That(path, Is.EqualTo(expectedPath), "Should combine directory and media key"); + } + + [Test] + public async Task AddEntryAsync_UnsupportedMediaType_ThrowsArgumentException() + { + // Arrange + var vault = await TestMediaVaultWrapper.FromAsync(TestDirectory); + Assert.That(vault, Is.Not.Null, "Vault should be created"); + + var entryKey = CreateTestEntryKey("test"); + var unsupportedMedia = new object(); // Not a supported media type + + // Act & Assert + Assert.ThrowsAsync(async () => + await vault!.AddEntryAsync(MediaVaultType.Image, entryKey, unsupportedMedia), + "Should throw for unsupported media type"); + } + + [Test] + public async Task AddEntryAsync_ValidMedia_UpdatesIndexAndCreatesFile() + { + // Arrange + var vault = await TestMediaVaultWrapper.FromAsync(TestDirectory); + Assert.That(vault, Is.Not.Null, "Vault should be created"); + + var entryKey = CreateTestEntryKey("test-media"); + var imageBinary = TestData.CreateTestImageBinary(1.0); // Use existing test data helper + + // Act + await vault!.AddEntryAsync(MediaVaultType.Image, entryKey, imageBinary); + + // Assert + Assert.That(vault.HasIndexEntry(entryKey), Is.True, "Should add entry to index"); + + var expectedFilePath = Path.Combine(TestDirectory, "test-media.png"); + AssertMediaFileExists(expectedFilePath, imageBinary.Buffer); + } + } + + /// + /// Tests for MediaVault error handling and edge cases - Interface Segregation + /// + [TestFixture] + public class MediaVaultErrorHandlingTests : MediaVaultTestBase + { + [Test] + public async Task ImageVault_FromAsync_CorruptedIndexFile_RecreatesIndex() + { + // Arrange - Create a corrupted index file + var indexPath = Path.Combine(TestDirectory, "index"); + await File.WriteAllTextAsync(indexPath, "{ corrupted json }"); + + // Act - Should handle corruption gracefully by recreating + var vault = await ImageVault.FromAsync(TestDirectory); + + // Assert + Assert.That(vault, Is.Not.Null, "Should create vault even with corrupted index"); + Assert.That(vault!.GetIndexSize(), Is.EqualTo(0), "Should have empty index after recreation"); + } + + [Test] + public async Task GetEntryAsync_CorruptedMediaFile_HandlesGracefully() + { + // Arrange + var vault = await ImageVault.FromAsync(TestDirectory); + var entryKey = CreateTestEntryKey("corrupted-file"); + var imageBinary = TestData.CreateTestImageBinary(1.0); + + await vault!.AddEntryAsync(MediaVaultType.Image, entryKey, imageBinary); + + // Corrupt the media file + var filePath = Path.Combine(TestDirectory, "corrupted-file.png"); + await File.WriteAllTextAsync(filePath, "corrupted data"); + + // Act & Assert - Should not throw, but behavior may vary + Assert.DoesNotThrowAsync(async () => + { + await vault.GetEntryAsync(MediaVaultType.Image, entryKey); + }, "Should handle corrupted files gracefully"); + } + + [Test] + public async Task AddEntryAsync_DiskSpaceIssue_HandlesGracefully() + { + // This test is difficult to simulate reliably across platforms + // Instead, we test with very large buffers that might cause issues + + // Arrange + var vault = await ImageVault.FromAsync(TestDirectory); + var entryKey = CreateTestEntryKey("large-file"); + + // Create a reasonably large buffer (not too large to cause test issues) + var largeBuffer = new byte[1024 * 1024]; // 1MB + Array.Fill(largeBuffer, 0xFF); + + var largeBinary = new ImageBinary(new ImageBinaryParams(largeBuffer, largeBuffer.Length, ".png", 1.0)); + + // Act & Assert - Should not throw exceptions + Assert.DoesNotThrowAsync(async () => + { + await vault!.AddEntryAsync(MediaVaultType.Image, entryKey, largeBinary); + }, "Should handle large files gracefully"); + } + + [Test] + public async Task GetEntryAsync_ConcurrentAccess_HandlesGracefully() + { + // Arrange + var vault = await ImageVault.FromAsync(TestDirectory); + var entryKey = CreateTestEntryKey("concurrent-test"); + var imageBinary = TestData.CreateTestImageBinary(1.0); + + await vault!.AddEntryAsync(MediaVaultType.Image, entryKey, imageBinary); + + // Act - Multiple concurrent reads + var tasks = new List>(); + for (int i = 0; i < 10; i++) + { + tasks.Add(vault.GetEntryAsync(MediaVaultType.Image, entryKey)); + } + + var results = await Task.WhenAll(tasks); + + // Assert + Assert.That(results.Length, Is.EqualTo(10), "Should complete all concurrent reads"); + foreach (var result in results) + { + Assert.That(result, Is.Not.Null, "Each concurrent read should succeed"); + Assert.That(result!.Buffer, Is.EqualTo(imageBinary.Buffer), "Each result should have correct data"); + } + } + } + + /// + /// Integration tests for MediaVault with FileDatabase - Dependency Inversion + /// + [TestFixture] + public class MediaVaultIntegrationTests : MediaVaultTestBase + { + [Test] + public async Task MediaVault_IntegratesWithFileDatabase_WorksEndToEnd() + { + // This test verifies that MediaVault works correctly when used through FileDatabase + + // Arrange + var database = await FileDatabase.FromAsync(TestDirectory); + var vaultKey = new EntryKey("test-vault", MediaVaultType.Image); + var entryKey = new EntryKey("test-image", MediaVaultType.Image); + var imageBinary = TestData.CreateTestImageBinary(1.5); + + // Act + await database!.CreateVaultAsync(vaultKey); + await database.RegisterResourceAsync(MediaVaultType.Image, vaultKey, entryKey, imageBinary); + var retrievedImage = await database.LoadResourceAsync(MediaVaultType.Image, vaultKey, entryKey); + + // Assert + Assert.That(retrievedImage, Is.Not.Null, "Should retrieve image through database"); + Assert.That(retrievedImage!.Buffer, Is.EqualTo(imageBinary.Buffer), "Retrieved data should match original"); + Assert.That(retrievedImage.AspectRatio, Is.EqualTo(imageBinary.AspectRatio), "Metadata should be preserved"); + + // Verify vault was created correctly + var vault = database.GetVault(vaultKey); + Assert.That(vault, Is.Not.Null, "Vault should exist in database"); + Assert.That(vault, Is.TypeOf(), "Should be ImageVault type"); + } + + [Test] + public async Task MediaVault_PersistenceAcrossRestarts_MaintainsData() + { + // Test that vault data persists when database is reloaded + + // Arrange - Create and populate vault + var database1 = await FileDatabase.FromAsync(TestDirectory); + var vaultKey = new EntryKey("persistent-vault", MediaVaultType.Image); + var entryKey = new EntryKey("persistent-image", MediaVaultType.Image); + var imageBinary = TestData.CreateTestImageBinary(2.0); + + await database1!.CreateVaultAsync(vaultKey); + await database1.RegisterResourceAsync(MediaVaultType.Image, vaultKey, entryKey, imageBinary); + + // Act - Reload database + var database2 = await FileDatabase.FromAsync(TestDirectory); + var retrievedImage = await database2!.LoadResourceAsync(MediaVaultType.Image, vaultKey, entryKey); + + // Assert + Assert.That(retrievedImage, Is.Not.Null, "Should retrieve image after database reload"); + Assert.That(retrievedImage!.Buffer, Is.EqualTo(imageBinary.Buffer), "Data should persist across restarts"); + Assert.That(retrievedImage.AspectRatio, Is.EqualTo(imageBinary.AspectRatio), "Metadata should persist"); + } + } +} diff --git a/DeepDrftTests/ModelTests.cs b/DeepDrftTests/ModelTests.cs index e20d624..1c2828f 100644 --- a/DeepDrftTests/ModelTests.cs +++ b/DeepDrftTests/ModelTests.cs @@ -150,6 +150,69 @@ public class ModelTests var decodedBuffer = Convert.FromBase64String(dto.Base64); Assert.That(decodedBuffer, Is.EqualTo(imageBinary.Buffer), "Decoded buffer should match original"); } + + [Test] + public void AudioBinary_CanBeCreated() + { + // Arrange + var buffer = TestData.TestPngBytes; + var size = buffer.Length; + var extension = ".mp3"; + var duration = 240.5; + var bitrate = 192; + var parameters = new AudioBinaryParams(buffer, size, extension, duration, bitrate); + + // Act + var audioBinary = new AudioBinary(parameters); + + // Assert + Assert.That(audioBinary.Buffer, Is.EqualTo(buffer), "Buffer should match"); + Assert.That(audioBinary.Size, Is.EqualTo(size), "Size should match"); + Assert.That(audioBinary.Extension, Is.EqualTo(extension), "Extension should match"); + Assert.That(audioBinary.Duration, Is.EqualTo(duration), "Duration should match"); + Assert.That(audioBinary.Bitrate, Is.EqualTo(bitrate), "Bitrate should match"); + } + + [Test] + public void AudioBinary_CanBeCreatedFromDto() + { + // Arrange + var originalBuffer = TestData.TestPngBytes; + var base64Data = Convert.ToBase64String(originalBuffer); + var duration = 180.0; + var bitrate = 256; + var dto = new AudioBinaryDto(base64Data, originalBuffer.Length, "audio/mpeg", duration, bitrate); + + // Act + var audioBinary = AudioBinary.From(dto); + + // Assert + Assert.That(audioBinary.Size, Is.EqualTo(originalBuffer.Length), "Size should match"); + Assert.That(audioBinary.Buffer, Is.EqualTo(originalBuffer), "Buffer should match original"); + Assert.That(audioBinary.Extension, Is.EqualTo(".mp3"), "Extension should match"); + Assert.That(audioBinary.Duration, Is.EqualTo(duration), "Duration should match"); + Assert.That(audioBinary.Bitrate, Is.EqualTo(bitrate), "Bitrate should match"); + } + + [Test] + public void AudioBinaryDto_CanBeCreatedFromAudioBinary() + { + // Arrange + var audioBinary = TestData.CreateTestAudioBinary(300.5, 128); + + // Act + var dto = new AudioBinaryDto(audioBinary); + + // Assert + Assert.That(dto.Size, Is.EqualTo(audioBinary.Size), "Size should match"); + Assert.That(dto.Mime, Is.EqualTo(MimeTypeExtensions.GetMimeType(audioBinary.Extension)), "MIME type should match"); + Assert.That(dto.Duration, Is.EqualTo(audioBinary.Duration), "Duration should match"); + Assert.That(dto.Bitrate, Is.EqualTo(audioBinary.Bitrate), "Bitrate should match"); + + // Verify base64 encoding + var decodedBuffer = Convert.FromBase64String(dto.Base64); + Assert.That(decodedBuffer, Is.EqualTo(audioBinary.Buffer), "Decoded buffer should match original"); + } } [TestFixture] @@ -188,23 +251,111 @@ public class ModelTests } [Test] - public void MetaDataFactory_CreatesCorrectTypes() + public void MetaDataFactory_CreatesMediaMetaData() { // Arrange var key = "test"; var extension = ".png"; - var aspectRatio = 2.0; // Act - var mediaMetaData = MetaDataFactory.Create(MediaVaultType.Media, key, extension, 0.0); - var imageMetaData = MetaDataFactory.Create(MediaVaultType.Image, key, extension, aspectRatio); + var mediaMetaData = MetaDataFactory.Create(MediaVaultType.Media, key, extension); // Assert Assert.That(mediaMetaData, Is.TypeOf(), "Should create MetaData for Media type"); - Assert.That(imageMetaData, Is.TypeOf(), "Should create ImageMetaData for Image type"); + Assert.That(mediaMetaData.MediaKey, Is.EqualTo(key), "MediaKey should match"); + Assert.That(mediaMetaData.Extension, Is.EqualTo(extension), "Extension should match"); + } - var typedImageMetaData = (ImageMetaData)imageMetaData; - Assert.That(typedImageMetaData.AspectRatio, Is.EqualTo(aspectRatio), "Aspect ratio should be set"); + [Test] + public void MetaDataFactory_CreatesImageMetaData() + { + // Arrange + var key = "test-image"; + var extension = ".png"; + var aspectRatio = 2.0; + + // Act + var imageMetaData = MetaDataFactory.CreateImageMetaData(key, extension, aspectRatio); + + // Assert + Assert.That(imageMetaData, Is.TypeOf(), "Should create ImageMetaData for Image type"); + Assert.That(imageMetaData.MediaKey, Is.EqualTo(key), "MediaKey should match"); + Assert.That(imageMetaData.Extension, Is.EqualTo(extension), "Extension should match"); + Assert.That(imageMetaData.AspectRatio, Is.EqualTo(aspectRatio), "Aspect ratio should be set"); + } + + [Test] + public void MetaDataFactory_CreatesAudioMetaData() + { + // Arrange + var key = "test-audio"; + var extension = ".mp3"; + var duration = 120.0; + var bitrate = 320; + + // Act + var audioMetaData = MetaDataFactory.CreateAudioMetaData(key, extension, duration, bitrate); + + // Assert + Assert.That(audioMetaData, Is.TypeOf(), "Should create AudioMetaData for Audio type"); + Assert.That(audioMetaData.MediaKey, Is.EqualTo(key), "MediaKey should match"); + Assert.That(audioMetaData.Extension, Is.EqualTo(extension), "Extension should match"); + Assert.That(audioMetaData.Duration, Is.EqualTo(duration), "Duration should be set"); + Assert.That(audioMetaData.Bitrate, Is.EqualTo(bitrate), "Bitrate should be set"); + } + + [Test] + public void AudioMetaData_CanBeCreated() + { + // Arrange + var key = "test-audio"; + var extension = ".mp3"; + var duration = 180.5; + var bitrate = 256; + + // Act + var audioMetaData = new AudioMetaData(key, extension, duration, bitrate); + + // Assert + Assert.That(audioMetaData.MediaKey, Is.EqualTo(key), "MediaKey should match"); + Assert.That(audioMetaData.Extension, Is.EqualTo(extension), "Extension should match"); + Assert.That(audioMetaData.Duration, Is.EqualTo(duration), "Duration should match"); + Assert.That(audioMetaData.Bitrate, Is.EqualTo(bitrate), "Bitrate should match"); + } + + [Test] + public void MetaDataFactory_CreateFromMedia_CreatesImageMetaData() + { + // Arrange + var key = "test-image"; + var extension = ".png"; + var imageBinary = TestData.CreateTestImageBinary(1.77); + + // Act + var metaData = MetaDataFactory.CreateFromMedia(MediaVaultType.Image, key, extension, imageBinary); + + // Assert + Assert.That(metaData, Is.TypeOf(), "Should create ImageMetaData from ImageBinary"); + var imageMetaData = (ImageMetaData)metaData; + Assert.That(imageMetaData.AspectRatio, Is.EqualTo(1.77), "Should extract aspect ratio from ImageBinary"); + } + + [Test] + public void MetaDataFactory_CreateFromMedia_CreatesAudioMetaData() + { + // Arrange + var key = "test-audio"; + var extension = ".mp3"; + var audioBinary = TestData.CreateTestAudioBinary(240.5, 192); + + // Act + var metaData = MetaDataFactory.CreateFromMedia(MediaVaultType.Audio, key, extension, audioBinary); + + // Assert + Assert.That(metaData, Is.TypeOf(), "Should create AudioMetaData from AudioBinary"); + var audioMetaData = (AudioMetaData)metaData; + Assert.That(audioMetaData.Duration, Is.EqualTo(240.5), "Should extract duration from AudioBinary"); + Assert.That(audioMetaData.Bitrate, Is.EqualTo(192), "Should extract bitrate from AudioBinary"); } } @@ -212,7 +363,7 @@ public class ModelTests public class MediaFactoryTests { [Test] - public void MediaBinaryFactory_CreatesCorrectTypes() + public void MediaBinaryFactory_CreatesMediaBinary() { // Arrange var buffer = TestData.TestPngBytes; @@ -221,17 +372,60 @@ public class ModelTests // Act var mediaParams = new MediaBinaryParams(buffer, size, extension); - var imageParams = new ImageBinaryParams(buffer, size, extension, 1.0); - var mediaBinary = FileBinaryFactory.Create(MediaVaultType.Media, mediaParams); - var imageBinary = FileBinaryFactory.Create(MediaVaultType.Image, imageParams); // Assert Assert.That(mediaBinary, Is.TypeOf(), "Should create MediaBinary for Media type"); - Assert.That(imageBinary, Is.TypeOf(), "Should create ImageBinary for Image type"); + var typedMediaBinary = (MediaBinary)mediaBinary; + Assert.That(typedMediaBinary.Buffer, Is.EqualTo(buffer), "Buffer should match"); + Assert.That(typedMediaBinary.Size, Is.EqualTo(size), "Size should match"); + Assert.That(typedMediaBinary.Extension, Is.EqualTo(extension), "Extension should match"); + } + [Test] + public void MediaBinaryFactory_CreatesImageBinary() + { + // Arrange + var buffer = TestData.TestPngBytes; + var size = buffer.Length; + var extension = ".png"; + var aspectRatio = 1.77; + + // Act + var imageParams = new ImageBinaryParams(buffer, size, extension, aspectRatio); + var imageBinary = FileBinaryFactory.Create(MediaVaultType.Image, imageParams); + + // Assert + Assert.That(imageBinary, Is.TypeOf(), "Should create ImageBinary for Image type"); var typedImageBinary = (ImageBinary)imageBinary; - Assert.That(typedImageBinary.AspectRatio, Is.EqualTo(1.0), "Aspect ratio should be set"); + Assert.That(typedImageBinary.Buffer, Is.EqualTo(buffer), "Buffer should match"); + Assert.That(typedImageBinary.Size, Is.EqualTo(size), "Size should match"); + Assert.That(typedImageBinary.Extension, Is.EqualTo(extension), "Extension should match"); + Assert.That(typedImageBinary.AspectRatio, Is.EqualTo(aspectRatio), "Aspect ratio should be set"); + } + + [Test] + public void MediaBinaryFactory_CreatesAudioBinary() + { + // Arrange + var buffer = TestData.TestPngBytes; + var size = buffer.Length; + var extension = ".mp3"; + var duration = 180.5; + var bitrate = 256; + + // Act + var audioParams = new AudioBinaryParams(buffer, size, extension, duration, bitrate); + var audioBinary = FileBinaryFactory.Create(MediaVaultType.Audio, audioParams); + + // Assert + Assert.That(audioBinary, Is.TypeOf(), "Should create AudioBinary for Audio type"); + var typedAudioBinary = (AudioBinary)audioBinary; + Assert.That(typedAudioBinary.Buffer, Is.EqualTo(buffer), "Buffer should match"); + Assert.That(typedAudioBinary.Size, Is.EqualTo(size), "Size should match"); + Assert.That(typedAudioBinary.Extension, Is.EqualTo(extension), "Extension should match"); + Assert.That(typedAudioBinary.Duration, Is.EqualTo(duration), "Duration should be set"); + Assert.That(typedAudioBinary.Bitrate, Is.EqualTo(bitrate), "Bitrate should be set"); } [Test] diff --git a/DeepDrftTests/SimpleMediaTypeRegistryTests.cs b/DeepDrftTests/SimpleMediaTypeRegistryTests.cs new file mode 100644 index 0000000..1177464 --- /dev/null +++ b/DeepDrftTests/SimpleMediaTypeRegistryTests.cs @@ -0,0 +1,618 @@ +using DeepDrftContent.FileDatabase.Models; +using DeepDrftContent.FileDatabase.Services; + +namespace DeepDrftTests; + +/// +/// SOLID, DRY tests for SimpleMediaTypeRegistry +/// Follows Single Responsibility: Each test class tests one registry concern +/// Follows Open/Closed: Tests extensibility patterns and type safety +/// Follows Interface Segregation: Tests focused interface methods +/// +[TestFixture] +public class SimpleMediaTypeRegistryTests +{ + /// + /// Base class for registry tests - DRY principle + /// + public abstract class RegistryTestBase + { + protected SimpleMediaTypeRegistry Registry { get; private set; } = null!; + protected string TestDirectory { get; private set; } = null!; + + [SetUp] + public virtual void SetUp() + { + Registry = new SimpleMediaTypeRegistry(); + TestDirectory = Path.Combine(Path.GetTempPath(), "DeepDrftTests", "Registry", Guid.NewGuid().ToString()); + Directory.CreateDirectory(TestDirectory); + } + + [TearDown] + public virtual void TearDown() + { + if (Directory.Exists(TestDirectory)) + { + try { Directory.Delete(TestDirectory, true); } catch { /* Ignore cleanup errors */ } + } + } + + /// + /// Helper method to create test binary objects using existing factories - DRY principle + /// + protected T CreateTestBinary(MediaVaultType vaultType) where T : FileBinary + { + var parameters = CreateTestParams(vaultType); + return (T)Registry.CreateBinary(vaultType, parameters); + } + + /// + /// Helper method to create test parameters using TestData - DRY principle + /// + protected object CreateTestParams(MediaVaultType vaultType) + { + return vaultType switch + { + MediaVaultType.Media => new MediaBinaryParams(TestData.TestPngBytes, TestData.TestPngBytes.Length, ".dat"), + MediaVaultType.Image => new ImageBinaryParams(TestData.TestPngBytes, TestData.TestPngBytes.Length, ".png", 1.0), + MediaVaultType.Audio => new AudioBinaryParams(TestData.TestPngBytes, TestData.TestPngBytes.Length, ".mp3", 120.0, 320), + _ => throw new ArgumentException($"Unsupported vault type: {vaultType}") + }; + } + + /// + /// Helper method to create test DTOs - DRY principle + /// + protected object CreateTestDto(MediaVaultType vaultType) + { + var base64Data = Convert.ToBase64String(TestData.TestPngBytes); + + return vaultType switch + { + MediaVaultType.Media => new MediaBinaryDto(base64Data, TestData.TestPngBytes.Length, "application/octet-stream"), + MediaVaultType.Image => new ImageBinaryDto(base64Data, TestData.TestPngBytes.Length, "image/png", 1.0), + MediaVaultType.Audio => new AudioBinaryDto(base64Data, TestData.TestPngBytes.Length, "audio/mpeg", 120.0, 320), + _ => throw new ArgumentException($"Unsupported vault type: {vaultType}") + }; + } + + /// + /// Helper method to create test metadata using existing factories - DRY principle + /// + protected MetaData CreateTestMetaData(MediaVaultType vaultType, string key = "test", string extension = ".png") + { + // Use the registry's metadata creation for consistency + var binary = CreateTestBinary(vaultType); + return Registry.CreateMetaDataFromMedia(vaultType, key, extension, binary); + } + } + + /// + /// Tests for binary creation functionality - Single Responsibility + /// + [TestFixture] + public class BinaryCreationTests : RegistryTestBase + { + [Test] + public void CreateBinary_MediaVaultType_CreatesMediaBinary() + { + // Arrange + var parameters = CreateTestParams(MediaVaultType.Media); + + // Act + var binary = Registry.CreateBinary(MediaVaultType.Media, parameters); + + // Assert + Assert.That(binary, Is.Not.Null, "Binary should be created"); + Assert.That(binary, Is.TypeOf(), "Should create MediaBinary"); + + var mediaBinary = (MediaBinary)binary; + Assert.That(mediaBinary.Buffer.Length, Is.EqualTo(TestData.TestPngBytes.Length), "Buffer should match"); + Assert.That(mediaBinary.Extension, Is.EqualTo(".dat"), "Extension should match"); + } + + [Test] + public void CreateBinary_ImageVaultType_CreatesImageBinary() + { + // Arrange + var parameters = CreateTestParams(MediaVaultType.Image); + + // Act + var binary = Registry.CreateBinary(MediaVaultType.Image, parameters); + + // Assert + Assert.That(binary, Is.Not.Null, "Binary should be created"); + Assert.That(binary, Is.TypeOf(), "Should create ImageBinary"); + + var imageBinary = (ImageBinary)binary; + Assert.That(imageBinary.Buffer.Length, Is.EqualTo(TestData.TestPngBytes.Length), "Buffer should match"); + Assert.That(imageBinary.Extension, Is.EqualTo(".png"), "Extension should match"); + Assert.That(imageBinary.AspectRatio, Is.EqualTo(1.0), "Aspect ratio should match"); + } + + [Test] + public void CreateBinary_AudioVaultType_CreatesAudioBinary() + { + // Arrange + var parameters = CreateTestParams(MediaVaultType.Audio); + + // Act + var binary = Registry.CreateBinary(MediaVaultType.Audio, parameters); + + // Assert + Assert.That(binary, Is.Not.Null, "Binary should be created"); + Assert.That(binary, Is.TypeOf(), "Should create AudioBinary"); + + var audioBinary = (AudioBinary)binary; + Assert.That(audioBinary.Buffer.Length, Is.EqualTo(TestData.TestPngBytes.Length), "Buffer should match"); + Assert.That(audioBinary.Extension, Is.EqualTo(".mp3"), "Extension should match"); + Assert.That(audioBinary.Duration, Is.EqualTo(120.0), "Duration should match"); + Assert.That(audioBinary.Bitrate, Is.EqualTo(320), "Bitrate should match"); + } + + [Test] + public void CreateBinary_InvalidVaultType_ThrowsArgumentException() + { + // Arrange + var invalidType = (MediaVaultType)999; + var parameters = CreateTestParams(MediaVaultType.Media); + + // Act & Assert + Assert.Throws(() => + Registry.CreateBinary(invalidType, parameters), + "Should throw for invalid vault type"); + } + + [Test] + public void CreateBinary_WrongParameterType_ThrowsException() + { + // Arrange - Use Image parameters for Media vault type + var imageParameters = CreateTestParams(MediaVaultType.Image); + + // Act & Assert - The registry is flexible and may handle type mismatches gracefully + // depending on the parameter compatibility + try + { + var result = Registry.CreateBinary(MediaVaultType.Media, imageParameters); + // If it succeeds, verify it created something reasonable + Assert.That(result, Is.Not.Null, "Should create some binary even with mismatched parameters"); + Assert.That(result, Is.TypeOf(), "Should create MediaBinary for Media vault type"); + } + catch (InvalidCastException) + { + Assert.Pass("Registry correctly threw InvalidCastException for parameter mismatch"); + } + catch (ArgumentException) + { + Assert.Pass("Registry correctly threw ArgumentException for parameter mismatch"); + } + } + } + + /// + /// Tests for DTO creation and conversion - Single Responsibility + /// + [TestFixture] + public class DtoCreationTests : RegistryTestBase + { + [Test] + public void CreateBinaryFromDto_MediaVaultType_CreatesMediaBinary() + { + // Arrange + var dto = CreateTestDto(MediaVaultType.Media); + + // Act + var binary = Registry.CreateBinaryFromDto(MediaVaultType.Media, dto); + + // Assert + Assert.That(binary, Is.Not.Null, "Binary should be created from DTO"); + Assert.That(binary, Is.TypeOf(), "Should create MediaBinary"); + + var mediaBinary = (MediaBinary)binary; + Assert.That(mediaBinary.Buffer, Is.EqualTo(TestData.TestPngBytes), "Buffer should match original"); + } + + [Test] + public void CreateBinaryFromDto_ImageVaultType_CreatesImageBinary() + { + // Arrange + var dto = CreateTestDto(MediaVaultType.Image); + + // Act + var binary = Registry.CreateBinaryFromDto(MediaVaultType.Image, dto); + + // Assert + Assert.That(binary, Is.Not.Null, "Binary should be created from DTO"); + Assert.That(binary, Is.TypeOf(), "Should create ImageBinary"); + + var imageBinary = (ImageBinary)binary; + Assert.That(imageBinary.Buffer, Is.EqualTo(TestData.TestPngBytes), "Buffer should match original"); + Assert.That(imageBinary.AspectRatio, Is.EqualTo(1.0), "Aspect ratio should be preserved"); + } + + [Test] + public void CreateBinaryFromDto_AudioVaultType_CreatesAudioBinary() + { + // Arrange + var dto = CreateTestDto(MediaVaultType.Audio); + + // Act + var binary = Registry.CreateBinaryFromDto(MediaVaultType.Audio, dto); + + // Assert + Assert.That(binary, Is.Not.Null, "Binary should be created from DTO"); + Assert.That(binary, Is.TypeOf(), "Should create AudioBinary"); + + var audioBinary = (AudioBinary)binary; + Assert.That(audioBinary.Buffer, Is.EqualTo(TestData.TestPngBytes), "Buffer should match original"); + Assert.That(audioBinary.Duration, Is.EqualTo(120.0), "Duration should be preserved"); + Assert.That(audioBinary.Bitrate, Is.EqualTo(320), "Bitrate should be preserved"); + } + + [Test] + public void CreateDto_MediaBinary_CreatesMediaBinaryDto() + { + // Arrange + var binary = CreateTestBinary(MediaVaultType.Media); + + // Act + var dto = Registry.CreateDto(MediaVaultType.Media, binary); + + // Assert + Assert.That(dto, Is.Not.Null, "DTO should be created"); + Assert.That(dto, Is.TypeOf(), "Should create MediaBinaryDto"); + + var mediaDto = (MediaBinaryDto)dto; + Assert.That(mediaDto.Size, Is.EqualTo(binary.Size), "Size should match"); + + var decodedBuffer = Convert.FromBase64String(mediaDto.Base64); + Assert.That(decodedBuffer, Is.EqualTo(binary.Buffer), "Decoded buffer should match original"); + } + + [Test] + public void CreateDto_ImageBinary_CreatesImageBinaryDto() + { + // Arrange + var binary = CreateTestBinary(MediaVaultType.Image); + + // Act + var dto = Registry.CreateDto(MediaVaultType.Image, binary); + + // Assert + Assert.That(dto, Is.Not.Null, "DTO should be created"); + Assert.That(dto, Is.TypeOf(), "Should create ImageBinaryDto"); + + var imageDto = (ImageBinaryDto)dto; + Assert.That(imageDto.Size, Is.EqualTo(binary.Size), "Size should match"); + Assert.That(imageDto.AspectRatio, Is.EqualTo(binary.AspectRatio), "Aspect ratio should match"); + } + + [Test] + public void CreateDto_AudioBinary_CreatesAudioBinaryDto() + { + // Arrange + var binary = CreateTestBinary(MediaVaultType.Audio); + + // Act + var dto = Registry.CreateDto(MediaVaultType.Audio, binary); + + // Assert + Assert.That(dto, Is.Not.Null, "DTO should be created"); + Assert.That(dto, Is.TypeOf(), "Should create AudioBinaryDto"); + + var audioDto = (AudioBinaryDto)dto; + Assert.That(audioDto.Size, Is.EqualTo(binary.Size), "Size should match"); + Assert.That(audioDto.Duration, Is.EqualTo(binary.Duration), "Duration should match"); + Assert.That(audioDto.Bitrate, Is.EqualTo(binary.Bitrate), "Bitrate should match"); + } + } + + /// + /// Tests for metadata creation - Single Responsibility + /// + [TestFixture] + public class MetaDataCreationTests : RegistryTestBase + { + [Test] + public void CreateMetaDataFromMedia_ImageBinary_CreatesImageMetaData() + { + // Arrange + var imageBinary = CreateTestBinary(MediaVaultType.Image); + const string key = "test-image"; + const string extension = ".png"; + + // Act + var metaData = Registry.CreateMetaDataFromMedia(MediaVaultType.Image, key, extension, imageBinary); + + // Assert + Assert.That(metaData, Is.Not.Null, "MetaData should be created"); + Assert.That(metaData, Is.TypeOf(), "Should create ImageMetaData"); + + var imageMetaData = (ImageMetaData)metaData; + Assert.That(imageMetaData.MediaKey, Is.EqualTo(key), "Key should match"); + Assert.That(imageMetaData.Extension, Is.EqualTo(extension), "Extension should match"); + Assert.That(imageMetaData.AspectRatio, Is.EqualTo(imageBinary.AspectRatio), "Should extract aspect ratio"); + } + + [Test] + public void CreateMetaDataFromMedia_AudioBinary_CreatesAudioMetaData() + { + // Arrange + var audioBinary = CreateTestBinary(MediaVaultType.Audio); + const string key = "test-audio"; + const string extension = ".mp3"; + + // Act + var metaData = Registry.CreateMetaDataFromMedia(MediaVaultType.Audio, key, extension, audioBinary); + + // Assert + Assert.That(metaData, Is.Not.Null, "MetaData should be created"); + Assert.That(metaData, Is.TypeOf(), "Should create AudioMetaData"); + + var audioMetaData = (AudioMetaData)metaData; + Assert.That(audioMetaData.MediaKey, Is.EqualTo(key), "Key should match"); + Assert.That(audioMetaData.Extension, Is.EqualTo(extension), "Extension should match"); + Assert.That(audioMetaData.Duration, Is.EqualTo(audioBinary.Duration), "Should extract duration"); + Assert.That(audioMetaData.Bitrate, Is.EqualTo(audioBinary.Bitrate), "Should extract bitrate"); + } + + [Test] + public void CreateMetaDataFromMedia_MediaBinary_CreatesBaseMetaData() + { + // Arrange + var mediaBinary = CreateTestBinary(MediaVaultType.Media); + const string key = "test-media"; + const string extension = ".dat"; + + // Act + var metaData = Registry.CreateMetaDataFromMedia(MediaVaultType.Media, key, extension, mediaBinary); + + // Assert + Assert.That(metaData, Is.Not.Null, "MetaData should be created"); + Assert.That(metaData, Is.TypeOf(), "Should create base MetaData"); + Assert.That(metaData.MediaKey, Is.EqualTo(key), "Key should match"); + Assert.That(metaData.Extension, Is.EqualTo(extension), "Extension should match"); + } + + [Test] + public void CreateMetaDataFromMedia_WrongMediaType_CreatesBaseMetaData() + { + // Arrange - Pass MediaBinary to Image vault type + var mediaBinary = CreateTestBinary(MediaVaultType.Media); + const string key = "test-wrong"; + const string extension = ".dat"; + + // Act + var metaData = Registry.CreateMetaDataFromMedia(MediaVaultType.Image, key, extension, mediaBinary); + + // Assert - Should fallback to base MetaData when media type doesn't match vault type + Assert.That(metaData, Is.Not.Null, "MetaData should be created"); + Assert.That(metaData, Is.TypeOf(), "Should create base MetaData as fallback"); + } + } + + /// + /// Tests for parameter creation - Interface Segregation + /// + [TestFixture] + public class ParameterCreationTests : RegistryTestBase + { + [Test] + public void CreateParams_ImageBinaryWithImageMetaData_CreatesImageBinaryParams() + { + // Arrange + var fileBinary = new FileBinary(new FileBinaryParams(TestData.TestPngBytes, TestData.TestPngBytes.Length)); + var imageMetaData = new ImageMetaData("test", ".png", 1.5); + + // Act + var parameters = Registry.CreateParams(MediaVaultType.Image, fileBinary, imageMetaData); + + // Assert + Assert.That(parameters, Is.Not.Null, "Parameters should be created"); + Assert.That(parameters, Is.TypeOf(), "Should create ImageBinaryParams"); + + var imageParams = (ImageBinaryParams)parameters; + Assert.That(imageParams.Buffer, Is.EqualTo(fileBinary.Buffer), "Buffer should match"); + Assert.That(imageParams.Size, Is.EqualTo(fileBinary.Size), "Size should match"); + Assert.That(imageParams.Extension, Is.EqualTo(imageMetaData.Extension), "Extension should match"); + Assert.That(imageParams.AspectRatio, Is.EqualTo(imageMetaData.AspectRatio), "Aspect ratio should match"); + } + + [Test] + public void CreateParams_AudioBinaryWithAudioMetaData_CreatesAudioBinaryParams() + { + // Arrange + var fileBinary = new FileBinary(new FileBinaryParams(TestData.TestPngBytes, TestData.TestPngBytes.Length)); + var audioMetaData = new AudioMetaData("test", ".mp3", 180.0, 256); + + // Act + var parameters = Registry.CreateParams(MediaVaultType.Audio, fileBinary, audioMetaData); + + // Assert + Assert.That(parameters, Is.Not.Null, "Parameters should be created"); + Assert.That(parameters, Is.TypeOf(), "Should create AudioBinaryParams"); + + var audioParams = (AudioBinaryParams)parameters; + Assert.That(audioParams.Buffer, Is.EqualTo(fileBinary.Buffer), "Buffer should match"); + Assert.That(audioParams.Size, Is.EqualTo(fileBinary.Size), "Size should match"); + Assert.That(audioParams.Extension, Is.EqualTo(audioMetaData.Extension), "Extension should match"); + Assert.That(audioParams.Duration, Is.EqualTo(audioMetaData.Duration), "Duration should match"); + Assert.That(audioParams.Bitrate, Is.EqualTo(audioMetaData.Bitrate), "Bitrate should match"); + } + + [Test] + public void CreateParams_WrongMetaDataType_ThrowsArgumentException() + { + // Arrange + var fileBinary = new FileBinary(new FileBinaryParams(TestData.TestPngBytes, TestData.TestPngBytes.Length)); + var baseMetaData = new MetaData("test", ".png"); // Wrong metadata type for Image vault + + // Act & Assert + Assert.Throws(() => + Registry.CreateParams(MediaVaultType.Image, fileBinary, baseMetaData), + "Should throw when metadata type doesn't match vault type requirements"); + } + } + + /// + /// Tests for vault creation - Dependency Inversion + /// + [TestFixture] + public class VaultCreationTests : RegistryTestBase + { + [Test] + public async Task CreateVaultAsync_ImageVaultType_CreatesImageVault() + { + // Act + var vault = await Registry.CreateVaultAsync(MediaVaultType.Image, TestDirectory); + + // Assert + Assert.That(vault, Is.Not.Null, "Vault should be created"); + Assert.That(vault, Is.TypeOf(), "Should create ImageVault"); + Assert.That(vault!.RootPath, Is.EqualTo(TestDirectory), "Should use provided path"); + } + + [Test] + public async Task CreateVaultAsync_AudioVaultType_CreatesAudioVault() + { + // Act + var vault = await Registry.CreateVaultAsync(MediaVaultType.Audio, TestDirectory); + + // Assert + Assert.That(vault, Is.Not.Null, "Vault should be created"); + Assert.That(vault, Is.TypeOf(), "Should create AudioVault"); + Assert.That(vault!.RootPath, Is.EqualTo(TestDirectory), "Should use provided path"); + } + + [Test] + public async Task CreateVaultAsync_MediaVaultType_ReturnsNull() + { + // Act + var vault = await Registry.CreateVaultAsync(MediaVaultType.Media, TestDirectory); + + // Assert + Assert.That(vault, Is.Null, "Should return null for unsupported Media vault type"); + } + } + + /// + /// Tests for type information retrieval - Interface Segregation + /// + [TestFixture] + public class TypeInformationTests : RegistryTestBase + { + [Test] + public void GetBinaryType_AllSupportedTypes_ReturnsCorrectTypes() + { + // Test all supported vault types + var expectedTypes = new Dictionary + { + { MediaVaultType.Media, typeof(MediaBinary) }, + { MediaVaultType.Image, typeof(ImageBinary) }, + { MediaVaultType.Audio, typeof(AudioBinary) } + }; + + foreach (var (vaultType, expectedType) in expectedTypes) + { + // Act + var actualType = Registry.GetBinaryType(vaultType); + + // Assert + Assert.That(actualType, Is.EqualTo(expectedType), $"Binary type for {vaultType} should be {expectedType.Name}"); + } + } + + [Test] + public void GetDtoType_AllSupportedTypes_ReturnsCorrectTypes() + { + // Test all supported vault types + var expectedTypes = new Dictionary + { + { MediaVaultType.Media, typeof(MediaBinaryDto) }, + { MediaVaultType.Image, typeof(ImageBinaryDto) }, + { MediaVaultType.Audio, typeof(AudioBinaryDto) } + }; + + foreach (var (vaultType, expectedType) in expectedTypes) + { + // Act + var actualType = Registry.GetDtoType(vaultType); + + // Assert + Assert.That(actualType, Is.EqualTo(expectedType), $"DTO type for {vaultType} should be {expectedType.Name}"); + } + } + + [Test] + public void GetParamsType_AllSupportedTypes_ReturnsCorrectTypes() + { + // Test all supported vault types + var expectedTypes = new Dictionary + { + { MediaVaultType.Media, typeof(MediaBinaryParams) }, + { MediaVaultType.Image, typeof(ImageBinaryParams) }, + { MediaVaultType.Audio, typeof(AudioBinaryParams) } + }; + + foreach (var (vaultType, expectedType) in expectedTypes) + { + // Act + var actualType = Registry.GetParamsType(vaultType); + + // Assert + Assert.That(actualType, Is.EqualTo(expectedType), $"Params type for {vaultType} should be {expectedType.Name}"); + } + } + + [Test] + public void GetMetaDataType_AllSupportedTypes_ReturnsCorrectTypes() + { + // Test all supported vault types + var expectedTypes = new Dictionary + { + { MediaVaultType.Media, typeof(MetaData) }, + { MediaVaultType.Image, typeof(ImageMetaData) }, + { MediaVaultType.Audio, typeof(AudioMetaData) } + }; + + foreach (var (vaultType, expectedType) in expectedTypes) + { + // Act + var actualType = Registry.GetMetaDataType(vaultType); + + // Assert + Assert.That(actualType, Is.EqualTo(expectedType), $"MetaData type for {vaultType} should be {expectedType.Name}"); + } + } + + [Test] + public void GetBinaryType_InvalidVaultType_ThrowsArgumentException() + { + // Arrange + var invalidType = (MediaVaultType)999; + + // Act & Assert + Assert.Throws(() => + Registry.GetBinaryType(invalidType), + "Should throw for invalid vault type"); + } + } + + /// + /// Tests for external registration (currently not implemented) - Open/Closed Principle + /// + [TestFixture] + public class ExternalRegistrationTests : RegistryTestBase + { + [Test] + public void RegisterMediaType_ExternalRegistration_ThrowsNotImplementedException() + { + // This test documents the current limitation and ensures we know when it changes + + // Act & Assert + Assert.Throws(() => + Registry.RegisterMediaType(MediaVaultType.Media), + "External registration should throw NotImplementedException until implemented"); + } + } +} diff --git a/DeepDrftTests/TestData.cs b/DeepDrftTests/TestData.cs index b531e03..b994719 100644 --- a/DeepDrftTests/TestData.cs +++ b/DeepDrftTests/TestData.cs @@ -31,6 +31,25 @@ public static class TestData return new ImageBinary(parameters); } + /// + /// Creates a test AudioBinary with mock audio data + /// + /// The duration in seconds + /// The bitrate in kbps + /// An AudioBinary instance with test data + public static AudioBinary CreateTestAudioBinary(double duration = 120.0, int bitrate = 320) + { + // Using PNG bytes as mock audio data for testing purposes + var parameters = new AudioBinaryParams( + Buffer: TestPngBytes, + Size: TestPngBytes.Length, + Extension: ".mp3", + Duration: duration, + Bitrate: bitrate + ); + return new AudioBinary(parameters); + } + /// /// Test entry keys used across tests ///