diff --git a/debug/TestApplication/DatabaseContext.cs b/debug/TestApplication/DatabaseContext.cs new file mode 100644 index 0000000..3abdad6 --- /dev/null +++ b/debug/TestApplication/DatabaseContext.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using TestApplication.Models; + +namespace TestApplication; + +public class DatabaseContext(DbContextOptions options) : DbContext(options) { + + public DbSet Users { get; set; } + + public DbSet Posts { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasMany(u => u.Posts) + .WithOne(p => p.Sender) + .OnDelete(DeleteBehavior.Cascade); + } +} \ No newline at end of file diff --git a/debug/TestApplication/Models/Post.cs b/debug/TestApplication/Models/Post.cs new file mode 100644 index 0000000..83bdbae --- /dev/null +++ b/debug/TestApplication/Models/Post.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace TestApplication.Models; + +public class Post { + [Key] + public Guid Id { get; } = Guid.CreateVersion7(); + + public required User Sender { get; set; } + + [MaxLength(5000)] + public required string Message { get; set; } +} \ No newline at end of file diff --git a/debug/TestApplication/Models/User.cs b/debug/TestApplication/Models/User.cs new file mode 100644 index 0000000..f20d288 --- /dev/null +++ b/debug/TestApplication/Models/User.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; + +namespace TestApplication.Models; + +public class User { + [Key] + public Guid Id { get; } = Guid.CreateVersion7(); + + [EmailAddress, MaxLength(25)] + public required string Email { get; set; } + + [MaxLength(25)] + public required string Username { get; set; } + + [MaxLength(64)] + public required string Password { get; set; } + + [MaxLength(25)] + public required string FirstName { get; set; } + + [MaxLength(25)] + public required string LastName { get; set; } + + [MaxLength(255)] + public string? Description { get; set; } + + public required DateOnly Birth { get; set; } = DateOnly.FromDateTime(DateTime.Today); + + public List Posts { get; set; } = new(); +} \ No newline at end of file diff --git a/debug/TestApplication/Program.cs b/debug/TestApplication/Program.cs index 5f6d302..c1c26f8 100644 --- a/debug/TestApplication/Program.cs +++ b/debug/TestApplication/Program.cs @@ -1,3 +1,7 @@ +using HopFrame.Core; +using HopFrame.Core.EFCore; +using Microsoft.EntityFrameworkCore; +using TestApplication; using TestApplication.Components; var builder = WebApplication.CreateBuilder(args); @@ -6,6 +10,15 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); +builder.Services.AddEntityFrameworkInMemoryDatabase(); +builder.Services.AddDbContext(options => { + options.UseInMemoryDatabase("testing"); +}); + +builder.Services.AddHopFrame(config => { + config.AddDbContext(); +}); + var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/debug/TestApplication/Properties/launchSettings.json b/debug/TestApplication/Properties/launchSettings.json index 81ef9cb..71028ca 100644 --- a/debug/TestApplication/Properties/launchSettings.json +++ b/debug/TestApplication/Properties/launchSettings.json @@ -4,7 +4,7 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "applicationUrl": "http://localhost:5281", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -13,7 +13,7 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "applicationUrl": "https://localhost:7126;http://localhost:5281", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/debug/TestApplication/TestApplication.csproj b/debug/TestApplication/TestApplication.csproj index 8d53cbd..7ee6932 100644 --- a/debug/TestApplication/TestApplication.csproj +++ b/debug/TestApplication/TestApplication.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/HopFrame.Core/Configuration/PropertyConfig.cs b/src/HopFrame.Core/Configuration/PropertyConfig.cs index 0bbe217..825c393 100644 --- a/src/HopFrame.Core/Configuration/PropertyConfig.cs +++ b/src/HopFrame.Core/Configuration/PropertyConfig.cs @@ -4,14 +4,17 @@ * The configuration for a single property */ public class PropertyConfig { - /** The unique identifier for the property (usually the real property name in the model) */ + /** [GENERATED] The unique identifier for the property (usually the real property name in the model) */ public required string Identifier { get; init; } - /** The displayed name of the Property */ + /** [GENERATED] The displayed name of the Property */ public required string DisplayName { get; set; } - /** The type of the property */ + /** [GENERATED] The real type of the property */ public required Type Type { get; set; } + + /** [GENERATED] The type as wich the property should be treated */ + public required PropertyType PropertyType { get; set; } /** Determines if the property will appear in the table */ public bool Listable { get; set; } = true; @@ -30,12 +33,58 @@ public class PropertyConfig { /** Determines if the property is visible in the creation or edit dialog */ public bool Creatable { get; set; } = true; - - /** Determines if the actual value should be displayed (useful for passwords) */ - public bool DisplayValue { get; set; } = true; - /** The place (from left to right) that the property will appear in the table and editor */ + /** [GENERATED] The place (from left to right) that the property will appear in the table and editor */ public int OrderIndex { get; set; } internal PropertyConfig() {} -} \ No newline at end of file +} + +/// +/// Used to distinguish between different input types in the frontend.
+/// Binary Format: First byte is used for additional properties, second byte identifies the real type +///
+[Flags] +public enum PropertyType : byte { + /** Used together with another type to indicate that the value can be null */ + Nullable = 0b10000000, + + /** Used together with another type to indicate that the property is a relation */ + Relation = 0b01000000, + + /** Used together with another type to indicate that the value is enumerable */ + List = 0b00100000, + + /** Indicates that the value is numeric */ + Numeric = 0x01, + + /** Indicates that the value is a boolean */ + Boolean = 0x02, + + /** Indicates that the value is a timestamp */ + DateTime = 0x03, + + /** Indicates that the value is a date */ + DateOnly = 0x04, + + /** Indicates that the value is a time of day */ + TimeOnly = 0x05, + + /** Indicates that the value is a list of fixed values */ + Enum = 0x06, + + /** Indicates that the value is a string */ + Text = 0x07, + + /** Indicates that the value is an email */ + Email = 0x08, + + /** Indicates that the value is a long string */ + TextArea = 0x09, + + /** Indicates that the value should be hidden */ + Password = 0x0A, + + /** Indicates that the value is a phone number */ + PhoneNumber = 0x0B +} diff --git a/src/HopFrame.Core/Configuration/TableConfig.cs b/src/HopFrame.Core/Configuration/TableConfig.cs index dc3ea4c..051c578 100644 --- a/src/HopFrame.Core/Configuration/TableConfig.cs +++ b/src/HopFrame.Core/Configuration/TableConfig.cs @@ -4,28 +4,28 @@ * The configuration for a table */ public class TableConfig { - /** The unique identifier for the table (usually the name of the model) */ + /** [GENERATED] The unique identifier for the table (usually the name of the model) */ public required string Identifier { get; init; } - /** The configurations for the properties of the model */ + /** [GENERATED] The configurations for the properties of the model */ public IList Properties { get; set; } = new List(); - /** The type of the model */ + /** [GENERATED] The type of the model */ public required Type TableType { get; set; } - /** The type identifier for the repository */ + /** [GENERATED] The type identifier for the repository */ public required Type RepositoryType { get; set; } - /** the url of the table page */ + /** [GENERATED] the url of the table page */ public required string Route { get; set; } - /** The displayed name of the table */ + /** [GENERATED] The displayed name of the table */ public required string DisplayName { get; set; } /** A short description for the table */ public string? Description { get; set; } - /** The place (from top to bottom) that the table will appear in on the sidebar */ + /** [GENERATED] The place (from top to bottom) that the table will appear in on the sidebar */ public int OrderIndex { get; set; } internal TableConfig() {} diff --git a/src/HopFrame.Core/Configurators/HopFrameConfigurator.cs b/src/HopFrame.Core/Configurators/HopFrameConfigurator.cs index 62b890e..23f9aab 100644 --- a/src/HopFrame.Core/Configurators/HopFrameConfigurator.cs +++ b/src/HopFrame.Core/Configurators/HopFrameConfigurator.cs @@ -13,6 +13,8 @@ public class HopFrameConfigurator(HopFrameConfig config, IServiceCollection serv /** The internal config that is modified */ public HopFrameConfig Config { get; } = config; + internal IServiceCollection Services { get; } = services; + /// /// Adds a new table to the configuration based on the provided repository /// @@ -22,7 +24,7 @@ public class HopFrameConfigurator(HopFrameConfig config, IServiceCollection serv public HopFrameConfigurator AddRepository(Action>? configurator = null) where TRepository : IHopFrameRepository where TModel : notnull { var table = ConfigurationHelper.InitializeTable(Config, typeof(TRepository), typeof(TModel)); Config.Tables.Add(table); - services.TryAddScoped(typeof(TRepository)); + Services.TryAddScoped(typeof(TRepository)); configurator?.Invoke(new TableConfigurator(table)); return this; } @@ -44,7 +46,7 @@ public class HopFrameConfigurator(HopFrameConfig config, IServiceCollection serv throw new ArgumentException($"Table '{config.Identifier}' has some validation errors:\n\t{string.Join("\n\t", errors)}"); Config.Tables.Add(config); - services.TryAddScoped(config.RepositoryType); + Services.TryAddScoped(config.RepositoryType); configurator?.Invoke(new TableConfigurator(config)); return this; } diff --git a/src/HopFrame.Core/Configurators/PropertyConfigurator.cs b/src/HopFrame.Core/Configurators/PropertyConfigurator.cs index f5bee78..d22d1e1 100644 --- a/src/HopFrame.Core/Configurators/PropertyConfigurator.cs +++ b/src/HopFrame.Core/Configurators/PropertyConfigurator.cs @@ -45,15 +45,18 @@ public class PropertyConfigurator(PropertyConfig config) { return this; } - /** */ - public PropertyConfigurator DisplayValue(bool displayValue) { - Config.DisplayValue = displayValue; - return this; - } - /** */ public PropertyConfigurator SetOrderIndex(int index) { Config.OrderIndex = index; return this; } + + /// + /// Sets the property type. The predefined modifiers (like nullable) persist. + /// If the property is a list or any other generic type, please use the enumerated type. + /// + public PropertyConfigurator SetType(PropertyType type) { + Config.PropertyType = (PropertyType)(((byte)Config.PropertyType & 0xF0) | ((byte)type & 0x0F)); + return this; + } } \ No newline at end of file diff --git a/src/HopFrame.Core/EFCore/DbConfigPopulator.cs b/src/HopFrame.Core/EFCore/DbConfigPopulator.cs new file mode 100644 index 0000000..19f378f --- /dev/null +++ b/src/HopFrame.Core/EFCore/DbConfigPopulator.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using HopFrame.Core.Configuration; +using HopFrame.Core.Helpers; +using Microsoft.Extensions.DependencyInjection; + +namespace HopFrame.Core.EFCore; + +internal static class DbConfigPopulator { + + public static string ConfigureRepository(HopFrameConfig global, IServiceCollection services, Type contextType, PropertyInfo tableProperty) { + var modelType = tableProperty.PropertyType.GenericTypeArguments.First(); + var repoType = typeof(EfCoreRepository<,>).MakeGenericType(modelType, contextType); + + services.AddScoped(repoType); + + var table = ConfigurationHelper.InitializeTable(global, repoType, modelType); + global.Tables.Add(table); + return table.Identifier; + } + + public static void CheckForRelations(HopFrameConfig global, TableConfig table) { + foreach (var property in table.Properties) { + var type = property.Type; + + if ((property.PropertyType & PropertyType.List) != 0 && type.IsGenericType) { + type = type.GenericTypeArguments.First(); + } + + if (global.Tables.Any(t => t.TableType == type)) + property.PropertyType |= PropertyType.Relation; + } + } + +} \ No newline at end of file diff --git a/src/HopFrame.Core/EFCore/EfCoreRepository.cs b/src/HopFrame.Core/EFCore/EfCoreRepository.cs new file mode 100644 index 0000000..b386b34 --- /dev/null +++ b/src/HopFrame.Core/EFCore/EfCoreRepository.cs @@ -0,0 +1,47 @@ +using HopFrame.Core.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace HopFrame.Core.EFCore; + +/// +/// The generic repository that handles data source communication for managed tables +/// +/// The model that is managed by the repo +/// The underlying context that handles database communication +public class EfCoreRepository(TContext context) : HopFrameRepository where TModel : class where TContext : DbContext { + + /// + public override Task> LoadPageAsync(int page, int perPage, CancellationToken ct = default) { + throw new NotImplementedException(); //TODO: Implement loading functionality + } + + /// + public override async Task CountAsync(CancellationToken ct = default) { + var table = context.Set(); + return await table.CountAsync(ct); + } + + /// + public override Task> SearchAsync(string searchTerm, int page, int perPage, CancellationToken ct = default) { + throw new NotImplementedException(); //TODO: Implement search functionality + } + + /// + public override async Task CreateAsync(TModel entry, CancellationToken ct = default) { + await context.AddAsync(entry, ct); + await context.SaveChangesAsync(ct); + } + + /// + public override async Task UpdateAsync(TModel entry, CancellationToken ct = default) { + context.Update(entry); + await context.SaveChangesAsync(ct); + } + + /// + public override async Task DeleteAsync(TModel entry, CancellationToken ct = default) { + context.Remove(entry); + await context.SaveChangesAsync(ct); + } + +} \ No newline at end of file diff --git a/src/HopFrame.Core/EFCore/HopFrameConfiguratorExtensions.cs b/src/HopFrame.Core/EFCore/HopFrameConfiguratorExtensions.cs new file mode 100644 index 0000000..8f5754d --- /dev/null +++ b/src/HopFrame.Core/EFCore/HopFrameConfiguratorExtensions.cs @@ -0,0 +1,44 @@ +using System.Linq.Expressions; +using HopFrame.Core.Configuration; +using HopFrame.Core.Configurators; +using HopFrame.Core.Helpers; +using Microsoft.EntityFrameworkCore; + +namespace HopFrame.Core.EFCore; + +/** Adds useful extensions to the to add managed repositories */ +public static class HopFrameConfiguratorExtensions { + + /// + /// Adds managed repositories for the selected (or all if none provided) tables + /// + /// The configurator for the current + /// The tables that should be configured (if none are provided, all tables will be added) + /// The already configured and injectable database context + public static HopFrameConfigurator AddDbContext(this HopFrameConfigurator configurator, params Expression>[] includedTables) where TDbContext : DbContext { + var contextType = typeof(TDbContext); + var properties = contextType.GetProperties() + .Where(p => p.PropertyType.IsGenericType) + .Where(p => p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)); + + if (includedTables.Length != 0) { + properties = includedTables.Select(ExpressionHelper.GetPropertyInfo); + } + + var tableIdentifiers = new List(); + foreach (var tableProperty in properties) { + var identifier = DbConfigPopulator.ConfigureRepository(configurator.Config, configurator.Services, contextType, tableProperty); + tableIdentifiers.Add(identifier); + } + + var createdTables = configurator.Config.Tables + .Where(t => tableIdentifiers.Contains(t.Identifier)) + .ToArray(); + foreach (var createdTable in createdTables) { + DbConfigPopulator.CheckForRelations(configurator.Config, createdTable); + } + + return configurator; + } + +} \ No newline at end of file diff --git a/src/HopFrame.Core/Helpers/ConfigurationHelper.cs b/src/HopFrame.Core/Helpers/ConfigurationHelper.cs index e6b5cc9..5823bf9 100644 --- a/src/HopFrame.Core/Helpers/ConfigurationHelper.cs +++ b/src/HopFrame.Core/Helpers/ConfigurationHelper.cs @@ -1,4 +1,7 @@ -using System.Reflection; +using System.Collections; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using System.Runtime.CompilerServices; using HopFrame.Core.Configuration; // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract @@ -28,7 +31,7 @@ internal static class ConfigurationHelper { return config; } - private static PropertyConfig InitializeProperty(TableConfig table, PropertyInfo property) { + public static PropertyConfig InitializeProperty(TableConfig table, PropertyInfo property) { var identifier = property.Name; if (table.Properties.Any(p => p.Identifier == identifier)) @@ -38,12 +41,60 @@ internal static class ConfigurationHelper { Identifier = identifier, Type = property.PropertyType, DisplayName = property.Name, - OrderIndex = table.Properties.Count + OrderIndex = table.Properties.Count, + PropertyType = InferPropertyType(property.PropertyType, property) }; return config; } + public static PropertyType InferPropertyType(Type realType, PropertyInfo info) { + byte modifiers = 0; + + if (Nullable.GetUnderlyingType(realType) != null) { + modifiers |= (byte)PropertyType.Nullable; + realType = Nullable.GetUnderlyingType(realType)!; + } + + if (info.CustomAttributes.Any(a => a.AttributeType == typeof(NullableAttribute))) { + modifiers |= (byte)PropertyType.Nullable; + } + + if ((realType.IsAssignableTo(typeof(IEnumerable)) || realType.IsAssignableTo(typeof(IEnumerable<>))) && realType != typeof(string)) { + modifiers |= (byte)PropertyType.List; + } + + if (realType.IsGenericType) { + realType = realType.GenericTypeArguments.First(); + } + + var type = PropertyType.Text; + var realTypeCode = Type.GetTypeCode(realType); + + if (realTypeCode == TypeCode.Boolean) + type = PropertyType.Boolean; + + if (realTypeCode == TypeCode.DateTime) + type = PropertyType.DateTime; + + if (realType == typeof(DateOnly)) + type = PropertyType.DateOnly; + + if (realType == typeof(TimeOnly)) + type = PropertyType.TimeOnly; + + if (realType.IsEnum) + type = PropertyType.Enum; + + if (realType.IsNumeric()) + type = PropertyType.Numeric; + + if (info.CustomAttributes.Any(a => a.AttributeType == typeof(EmailAddressAttribute))) + type = PropertyType.Email; + + return (PropertyType)((byte)type | modifiers); + } + public static IEnumerable ValidateTable(HopFrameConfig global, TableConfig config) { if (global.Tables.Any(t => t.Identifier == config.Identifier)) yield return $"Table identifier '{config.Identifier}' is not unique"; diff --git a/src/HopFrame.Core/Helpers/TypeExtensions.cs b/src/HopFrame.Core/Helpers/TypeExtensions.cs new file mode 100644 index 0000000..3c50341 --- /dev/null +++ b/src/HopFrame.Core/Helpers/TypeExtensions.cs @@ -0,0 +1,23 @@ +namespace HopFrame.Core.Helpers; + +internal static class TypeExtensions { + public static bool IsNumeric(this Type o) { + if (o.IsEnum) return false; + switch (Type.GetTypeCode(o)) { + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.Decimal: + case TypeCode.Double: + case TypeCode.Single: + return true; + default: + return false; + } + } +} \ No newline at end of file diff --git a/src/HopFrame.Core/HopFrame.Core.csproj b/src/HopFrame.Core/HopFrame.Core.csproj index d1881a3..a2c2218 100644 --- a/src/HopFrame.Core/HopFrame.Core.csproj +++ b/src/HopFrame.Core/HopFrame.Core.csproj @@ -12,6 +12,7 @@ + diff --git a/src/HopFrame.Core/Repositories/HopFrameRepository.cs b/src/HopFrame.Core/Repositories/HopFrameRepository.cs index 96b8489..db35cf9 100644 --- a/src/HopFrame.Core/Repositories/HopFrameRepository.cs +++ b/src/HopFrame.Core/Repositories/HopFrameRepository.cs @@ -3,7 +3,7 @@ namespace HopFrame.Core.Repositories; /** The base repository that provides access to the model dataset */ -public abstract class HopFrameRepository : IHopFrameRepository where TModel : notnull { +public abstract class HopFrameRepository : IHopFrameRepository where TModel : class { /** */ public abstract Task> LoadPageAsync(int page, int perPage, CancellationToken ct = default); @@ -15,18 +15,21 @@ public abstract class HopFrameRepository : IHopFrameRepository where TMo public abstract Task> SearchAsync(string searchTerm, int page, int perPage, CancellationToken ct = default); /** */ - public abstract Task CreateAsync(TModel entry, CancellationToken ct); + public abstract Task CreateAsync(TModel entry, CancellationToken ct = default); + + /** */ + public abstract Task UpdateAsync(TModel entry, CancellationToken ct = default); /** */ - public abstract Task DeleteAsync(TModel entry, CancellationToken ct); + public abstract Task DeleteAsync(TModel entry, CancellationToken ct = default); /** */ - public async Task LoadPageGenericAsync(int page, int perPage, CancellationToken ct = default) { + public async Task LoadPageGenericAsync(int page, int perPage, CancellationToken ct) { return await LoadPageAsync(page, perPage, ct); } /** */ - public async Task SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct = default) { + public async Task SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct) { return await SearchAsync(searchTerm, page, perPage, ct); } @@ -34,7 +37,12 @@ public abstract class HopFrameRepository : IHopFrameRepository where TMo public Task CreateGenericAsync(object entry, CancellationToken ct) { return CreateAsync((TModel)entry, ct); } - + + /** */ + public Task UpdateGenericAsync(object entry, CancellationToken ct) { + return UpdateAsync((TModel)entry, ct); + } + /** */ public Task DeleteGenericAsync(object entry, CancellationToken ct) { return DeleteAsync((TModel)entry, ct); diff --git a/src/HopFrame.Core/Repositories/IHopFrameRepository.cs b/src/HopFrame.Core/Repositories/IHopFrameRepository.cs index 7ffdd16..9ff8460 100644 --- a/src/HopFrame.Core/Repositories/IHopFrameRepository.cs +++ b/src/HopFrame.Core/Repositories/IHopFrameRepository.cs @@ -11,12 +11,12 @@ public interface IHopFrameRepository { /// /// The index of the current page (starts at 0) /// The amount of entries that should be loaded - public Task LoadPageGenericAsync(int page, int perPage, CancellationToken ct = default); + public Task LoadPageGenericAsync(int page, int perPage, CancellationToken ct); /// /// Returns the total amount of entries in the dataset /// - public Task CountAsync(CancellationToken ct = default); + public Task CountAsync(CancellationToken ct); /// /// Searches through the whole dataset and returns a page of matching entries @@ -24,7 +24,7 @@ public interface IHopFrameRepository { /// The search text provided by the user /// The index of the current page (starts at 0) /// The amount of entries that should be loaded - public Task SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct = default); + public Task SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct); /// @@ -32,6 +32,12 @@ public interface IHopFrameRepository { /// /// The entry that needs to be saved public Task CreateGenericAsync(object entry, CancellationToken ct); + + /// + /// Saves the changes made to the entry to the dataset + /// + /// The modified entry + public Task UpdateGenericAsync(object entry, CancellationToken ct); /// /// Deletes the provided entry from the dataset diff --git a/tests/HopFrame.Tests.Core/Configurators/HopFrameConfiguratorTests.cs b/tests/HopFrame.Tests.Core/Configurators/HopFrameConfiguratorTests.cs index ab4d3af..75d6ea1 100644 --- a/tests/HopFrame.Tests.Core/Configurators/HopFrameConfiguratorTests.cs +++ b/tests/HopFrame.Tests.Core/Configurators/HopFrameConfiguratorTests.cs @@ -8,18 +8,23 @@ namespace HopFrame.Tests.Core.Configurators; public class HopFrameConfiguratorTests { private class TestRepository : IHopFrameRepository { - public Task LoadPageGenericAsync(int page, int perPage, CancellationToken ct = default) { + public Task LoadPageGenericAsync(int page, int perPage, CancellationToken ct) { throw new NotImplementedException(); } - public Task CountAsync(CancellationToken ct = default) { + public Task CountAsync(CancellationToken ct) { throw new NotImplementedException(); } - public Task SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct = default) { + public Task SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct) { throw new NotImplementedException(); } public Task CreateGenericAsync(object entry, CancellationToken ct) { throw new NotImplementedException(); } + + public Task UpdateGenericAsync(object entry, CancellationToken ct) { + throw new NotImplementedException(); + } + public Task DeleteGenericAsync(object entry, CancellationToken ct) { throw new NotImplementedException(); } @@ -41,7 +46,8 @@ public class HopFrameConfiguratorTests { Identifier = "Id", DisplayName = "Id", Type = typeof(int), - OrderIndex = 0 + OrderIndex = 0, + PropertyType = PropertyType.Numeric } } }; diff --git a/tests/HopFrame.Tests.Core/Configurators/PropertyConfiguratorTests.cs b/tests/HopFrame.Tests.Core/Configurators/PropertyConfiguratorTests.cs new file mode 100644 index 0000000..5919b58 --- /dev/null +++ b/tests/HopFrame.Tests.Core/Configurators/PropertyConfiguratorTests.cs @@ -0,0 +1,85 @@ +using HopFrame.Core.Configuration; +using HopFrame.Core.Configurators; + +namespace HopFrame.Tests.Core.Configurators; + +public class PropertyConfiguratorTests { + private PropertyConfig CreateConfig(PropertyType type) + => new PropertyConfig { + Identifier = "Test", + DisplayName = "Test", + Type = typeof(string), + OrderIndex = 0, + PropertyType = type + }; + + [Fact] + public void SetType_ReplacesBaseType_AndPreservesModifiers() { + // Arrange: Nullable + List + Numeric + var original = PropertyType.Numeric | PropertyType.Nullable | PropertyType.List; + var config = CreateConfig(original); + + var configurator = new PropertyConfigurator(config); + + // Act: change base type to Text + configurator.SetType(PropertyType.Text); + + // Assert: modifiers remain, base type replaced + Assert.Equal( + PropertyType.Text | PropertyType.Nullable | PropertyType.List, + config.PropertyType + ); + } + + [Fact] + public void SetType_DoesNotAffectModifiers_WhenSettingSameBaseType() { + var original = PropertyType.Boolean | PropertyType.Nullable; + var config = CreateConfig(original); + + var configurator = new PropertyConfigurator(config); + + configurator.SetType(PropertyType.Boolean); + + Assert.Equal(original, config.PropertyType); + } + + [Fact] + public void SetType_CanChangeEnumToNumeric_WhileKeepingModifiers() { + var original = PropertyType.Enum | PropertyType.List; + var config = CreateConfig(original); + + var configurator = new PropertyConfigurator(config); + + configurator.SetType(PropertyType.Numeric); + + Assert.Equal( + PropertyType.Numeric | PropertyType.List, + config.PropertyType + ); + } + + [Fact] + public void SetType_CanChangeToEmail_AndPreserveNullable() { + var original = PropertyType.Text | PropertyType.Nullable; + var config = CreateConfig(original); + + var configurator = new PropertyConfigurator(config); + + configurator.SetType(PropertyType.Email); + + Assert.Equal( + PropertyType.Email | PropertyType.Nullable, + config.PropertyType + ); + } + + [Fact] + public void SetType_ReturnsConfigurator_ForFluentApi() { + var config = CreateConfig(PropertyType.Text); + var configurator = new PropertyConfigurator(config); + + var result = configurator.SetType(PropertyType.Numeric); + + Assert.Same(configurator, result); + } +} \ No newline at end of file diff --git a/tests/HopFrame.Tests.Core/EFCore/DbConfigPopulatorTests.cs b/tests/HopFrame.Tests.Core/EFCore/DbConfigPopulatorTests.cs new file mode 100644 index 0000000..224a090 --- /dev/null +++ b/tests/HopFrame.Tests.Core/EFCore/DbConfigPopulatorTests.cs @@ -0,0 +1,147 @@ +using HopFrame.Core.Configuration; +using HopFrame.Core.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace HopFrame.Tests.Core.EFCore; + +public class DbConfigPopulatorTests { + private class TestContext { } + + private class TestModel { + public int Id { get; set; } + } + + private class OtherModel { + public int X { get; set; } + } + + private HopFrameConfig CreateConfig(params TableConfig[] tables) + => new HopFrameConfig { Tables = tables.ToList() }; + + private TableConfig CreateTable(Type modelType) + => new TableConfig { + Identifier = modelType.Name, + TableType = modelType, + RepositoryType = typeof(object), + Route = modelType.Name.ToLower(), + DisplayName = modelType.Name, + OrderIndex = 0, + Properties = new List { + new PropertyConfig { + Identifier = "Id", + DisplayName = "Id", + Type = typeof(int), + PropertyType = PropertyType.Numeric + } + } + }; + + // ------------------------------------------------------------- + // ConfigureRepository + // ------------------------------------------------------------- + + [Fact] + public void ConfigureRepository_RegistersRepositoryType() { + var services = new ServiceCollection(); + var global = CreateConfig(); + + var prop = typeof(TestDbContext).GetProperty(nameof(TestDbContext.TestModels))!; + + var identifier = DbConfigPopulator.ConfigureRepository(global, services, typeof(TestDbContext), prop); + + var repoType = typeof(EfCoreRepository); + + Assert.Contains(services, d => d.ServiceType == repoType); + Assert.Single(global.Tables); + Assert.Equal(identifier, global.Tables[0].Identifier); + } + + [Fact] + public void ConfigureRepository_AddsTableToGlobalConfig() { + var services = new ServiceCollection(); + var global = CreateConfig(); + + var prop = typeof(TestDbContext).GetProperty(nameof(TestDbContext.TestModels))!; + + DbConfigPopulator.ConfigureRepository(global, services, typeof(TestDbContext), prop); + + Assert.Single(global.Tables); + Assert.Equal(typeof(TestModel), global.Tables[0].TableType); + } + + private class TestDbContext : DbContext { + public List TestModels { get; set; } = new(); + } + + // ------------------------------------------------------------- + // CheckForRelations + // ------------------------------------------------------------- + + private class RelationModel { + public OtherModel Single { get; set; } = new(); + public List Many { get; set; } = new(); + } + + [Fact] + public void CheckForRelations_SetsRelationFlag_ForSingleReference() { + var otherTable = CreateTable(typeof(OtherModel)); + var relationTable = CreateTable(typeof(RelationModel)); + + relationTable.Properties = new List { + new PropertyConfig { + Identifier = "Single", + DisplayName = "Single", + Type = typeof(OtherModel), + PropertyType = PropertyType.Text + } + }; + + var global = CreateConfig(otherTable, relationTable); + + DbConfigPopulator.CheckForRelations(global, relationTable); + + Assert.True((relationTable.Properties[0].PropertyType & PropertyType.Relation) != 0); + } + + [Fact] + public void CheckForRelations_SetsRelationFlag_ForListReference() { + var otherTable = CreateTable(typeof(OtherModel)); + var relationTable = CreateTable(typeof(RelationModel)); + + relationTable.Properties = new List { + new PropertyConfig { + Identifier = "Many", + DisplayName = "Many", + Type = typeof(List), + PropertyType = PropertyType.List + } + }; + + var global = CreateConfig(otherTable, relationTable); + + DbConfigPopulator.CheckForRelations(global, relationTable); + + Assert.True((relationTable.Properties[0].PropertyType & PropertyType.Relation) != 0); + } + + [Fact] + public void CheckForRelations_DoesNotSetRelationFlag_WhenNoMatchingTableExists() { + var relationTable = CreateTable(typeof(RelationModel)); + + relationTable.Properties = new List { + new PropertyConfig { + Identifier = "Single", + DisplayName = "Single", + Type = typeof(OtherModel), + PropertyType = PropertyType.Text + } + }; + + var global = CreateConfig(relationTable); + + DbConfigPopulator.CheckForRelations(global, relationTable); + + Assert.False((relationTable.Properties[0].PropertyType & PropertyType.Relation) != 0); + } +} \ No newline at end of file diff --git a/tests/HopFrame.Tests.Core/EFCore/HopFrameConfiguratorExtensionsTests.cs b/tests/HopFrame.Tests.Core/EFCore/HopFrameConfiguratorExtensionsTests.cs new file mode 100644 index 0000000..9567fdc --- /dev/null +++ b/tests/HopFrame.Tests.Core/EFCore/HopFrameConfiguratorExtensionsTests.cs @@ -0,0 +1,128 @@ +using HopFrame.Core.Configuration; +using HopFrame.Core.Configurators; +using HopFrame.Core.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace HopFrame.Tests.Core.EFCore; + +public class HopFrameConfiguratorExtensionsTests { + private class TestModelA { + public int Id { get; set; } + } + + private class TestModelB { + public int Id { get; set; } + } + + private class TestDbContext : DbContext { + public DbSet A { get; set; } = null!; + public DbSet B { get; set; } = null!; + } + + private HopFrameConfig CreateConfig() + => new HopFrameConfig { Tables = new List() }; + + // ------------------------------------------------------------- + // AddDbContext - all tables + // ------------------------------------------------------------- + + [Fact] + public void AddDbContext_AddsAllDbSets_WhenNoFilterProvided() { + var services = new ServiceCollection(); + var config = CreateConfig(); + var configurator = new HopFrameConfigurator(config, services); + + configurator.AddDbContext(); + + Assert.Equal(2, config.Tables.Count); + Assert.Contains(config.Tables, t => t.TableType == typeof(TestModelA)); + Assert.Contains(config.Tables, t => t.TableType == typeof(TestModelB)); + } + + [Fact] + public void AddDbContext_RegistersRepositoriesForAllDbSets() { + var services = new ServiceCollection(); + var config = CreateConfig(); + var configurator = new HopFrameConfigurator(config, services); + + configurator.AddDbContext(); + + Assert.Contains(services, d => d.ServiceType == typeof(EfCoreRepository)); + Assert.Contains(services, d => d.ServiceType == typeof(EfCoreRepository)); + } + + // ------------------------------------------------------------- + // AddDbContext - filtered tables + // ------------------------------------------------------------- + + [Fact] + public void AddDbContext_UsesOnlyIncludedTables_WhenFilterProvided() { + var services = new ServiceCollection(); + var config = CreateConfig(); + var configurator = new HopFrameConfigurator(config, services); + + configurator.AddDbContext(ctx => ctx.A + ); + + Assert.Single(config.Tables); + Assert.Equal(typeof(TestModelA), config.Tables[0].TableType); + } + + [Fact] + public void AddDbContext_RegistersOnlyFilteredRepositories() { + var services = new ServiceCollection(); + var config = CreateConfig(); + var configurator = new HopFrameConfigurator(config, services); + + configurator.AddDbContext(ctx => ctx.A + ); + + Assert.Contains(services, d => d.ServiceType == typeof(EfCoreRepository)); + Assert.DoesNotContain(services, d => d.ServiceType == typeof(EfCoreRepository)); + } + + // ------------------------------------------------------------- + // Relation detection + // ------------------------------------------------------------- + + private class RelationModel { + public TestModelA Single { get; set; } = null!; + public List Many { get; set; } = new(); + } + + private class RelationDbContext : DbContext { + public DbSet A { get; set; } = null!; + public DbSet B { get; set; } = null!; + public DbSet R { get; set; } = null!; + } + + [Fact] + public void AddDbContext_ChecksRelationsForCreatedTables() { + var services = new ServiceCollection(); + var config = CreateConfig(); + var configurator = new HopFrameConfigurator(config, services); + + configurator.AddDbContext(); + + var relationTable = config.Tables.Single(t => t.TableType == typeof(RelationModel)); + + // At least one property should have the Relation flag + Assert.Contains(relationTable.Properties, p => (p.PropertyType & PropertyType.Relation) != 0); + } + + // ------------------------------------------------------------- + // Fluent API + // ------------------------------------------------------------- + + [Fact] + public void AddDbContext_ReturnsConfigurator() { + var services = new ServiceCollection(); + var config = CreateConfig(); + var configurator = new HopFrameConfigurator(config, services); + + var result = configurator.AddDbContext(); + + Assert.Same(configurator, result); + } +} \ No newline at end of file diff --git a/tests/HopFrame.Tests.Core/Helpers/ConfigurationHelperTests.cs b/tests/HopFrame.Tests.Core/Helpers/ConfigurationHelperTests.cs index 90813cd..551eb11 100644 --- a/tests/HopFrame.Tests.Core/Helpers/ConfigurationHelperTests.cs +++ b/tests/HopFrame.Tests.Core/Helpers/ConfigurationHelperTests.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.ComponentModel.DataAnnotations; using HopFrame.Core.Configuration; using HopFrame.Core.Helpers; @@ -7,6 +7,28 @@ using HopFrame.Core.Helpers; namespace HopFrame.Tests.Core.Helpers; public class ConfigurationHelperTests { + private class PropertyTypeModel { + public int Number { get; set; } + public int? NullableNumber { get; set; } + public string Text { get; set; } = ""; + public bool Flag { get; set; } + public DateTime Timestamp { get; set; } + public DateOnly Date { get; set; } + public TimeOnly Time { get; set; } + public TestEnum EnumValue { get; set; } + public List Numbers { get; set; } = new(); + public IEnumerable Strings { get; set; } = new List(); + + [EmailAddress] + public string Email { get; set; } = ""; + } + + private enum TestEnum { + A, + B + } + + private HopFrameConfig CreateGlobal(params TableConfig[] tables) => new HopFrameConfig { Tables = tables.ToList() }; @@ -21,7 +43,8 @@ public class ConfigurationHelperTests { new PropertyConfig { Identifier = "Prop1", DisplayName = "Property 1", - Type = typeof(int) + Type = typeof(int), + PropertyType = PropertyType.Numeric } } }; @@ -92,12 +115,30 @@ public class ConfigurationHelperTests { Assert.Contains(config.Properties, p => p.Identifier == "Name"); } + [Fact] + public void InitializeTable_SetsPropertyTypes_ForAllProperties() { + var global = CreateGlobal(); + + var config = ConfigurationHelper.InitializeTable(global, typeof(string), typeof(PropertyTypeModel)); + + Assert.Contains(config.Properties, p => p.PropertyType == PropertyType.Numeric); + Assert.Contains(config.Properties, p => p.PropertyType == (PropertyType.Numeric | PropertyType.Nullable)); + Assert.Contains(config.Properties, p => p.PropertyType == PropertyType.Boolean); + Assert.Contains(config.Properties, p => p.PropertyType == PropertyType.DateTime); + Assert.Contains(config.Properties, p => p.PropertyType == PropertyType.DateOnly); + Assert.Contains(config.Properties, p => p.PropertyType == PropertyType.TimeOnly); + Assert.Contains(config.Properties, p => p.PropertyType == PropertyType.Enum); + Assert.Contains(config.Properties, p => p.PropertyType == (PropertyType.Numeric | PropertyType.List)); + Assert.Contains(config.Properties, p => p.PropertyType == (PropertyType.Text | PropertyType.List)); + Assert.Contains(config.Properties, p => p.PropertyType == PropertyType.Email); + } + [Fact] public void InitializeProperty_UsesPropertyNameAsIdentifier_WhenUnique() { var table = CreateDummyTable("T"); var property = typeof(TestModel).GetProperty(nameof(TestModel.Id))!; - var config = InvokeInitializeProperty(table, property); + var config = ConfigurationHelper.InitializeProperty(table, property); Assert.Equal("Id", config.Identifier); } @@ -105,12 +146,14 @@ public class ConfigurationHelperTests { [Fact] public void InitializeProperty_GeneratesGuid_WhenIdentifierAlreadyExists() { var table = CreateDummyTable("T"); - table.Properties.Add(new PropertyConfig - { Identifier = "Id", Type = typeof(int), DisplayName = "Id", OrderIndex = 0 }); + table.Properties.Add(new PropertyConfig { + Identifier = "Id", Type = typeof(int), DisplayName = "Id", OrderIndex = 0, + PropertyType = PropertyType.Numeric + }); var property = typeof(TestModel).GetProperty(nameof(TestModel.Id))!; - var config = InvokeInitializeProperty(table, property); + var config = ConfigurationHelper.InitializeProperty(table, property); Assert.NotEqual("Id", config.Identifier); Assert.True(Guid.TryParse(config.Identifier, out _)); @@ -121,7 +164,7 @@ public class ConfigurationHelperTests { var table = CreateDummyTable("T"); var property = typeof(TestModel).GetProperty(nameof(TestModel.Name))!; - var config = InvokeInitializeProperty(table, property); + var config = ConfigurationHelper.InitializeProperty(table, property); Assert.Equal("Name", config.DisplayName); Assert.Equal(typeof(string), config.Type); @@ -131,23 +174,28 @@ public class ConfigurationHelperTests { [Fact] public void InitializeProperty_SetsOrderIndex_ToCurrentPropertyCount() { var table = CreateDummyTable("T"); - table.Properties.Add(new PropertyConfig - { Identifier = "X", Type = typeof(int), DisplayName = "X", OrderIndex = 0 }); + table.Properties.Add(new PropertyConfig { + Identifier = "X", Type = typeof(int), DisplayName = "X", OrderIndex = 0, PropertyType = PropertyType.Numeric + }); var property = typeof(TestModel).GetProperty(nameof(TestModel.Name))!; - var config = InvokeInitializeProperty(table, property); + var config = ConfigurationHelper.InitializeProperty(table, property); Assert.Equal(1, config.OrderIndex); } - private PropertyConfig InvokeInitializeProperty(TableConfig table, PropertyInfo property) { - var method = typeof(ConfigurationHelper) - .GetMethod("InitializeProperty", BindingFlags.NonPublic | BindingFlags.Static)!; + [Fact] + public void InitializeProperty_SetsPropertyType_FromInferPropertyType() { + var table = CreateDummyTable("T"); + var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Number))!; - return (PropertyConfig)method.Invoke(null, [table, property])!; + var config = ConfigurationHelper.InitializeProperty(table, prop); + + Assert.Equal(PropertyType.Numeric, config.PropertyType); } + [Fact] public void ValidateTable_ReturnsError_WhenIdentifierNotUnique() { var config = CreateValidTable(); @@ -210,7 +258,8 @@ public class ConfigurationHelperTests { config.Properties.Add(new PropertyConfig { Identifier = "Prop1", DisplayName = "Duplicate", - Type = typeof(int) + Type = typeof(int), + PropertyType = PropertyType.Numeric }); var result = ConfigurationHelper.ValidateTable(CreateGlobal(), config).ToList(); @@ -246,4 +295,84 @@ public class ConfigurationHelperTests { Assert.Empty(result); } + + [Fact] + public void InferPropertyType_RecognizesNumeric() { + var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Number))!; + var result = ConfigurationHelper.InferPropertyType(typeof(int), prop); + + Assert.Equal(PropertyType.Numeric, result); + } + + [Fact] + public void InferPropertyType_RecognizesNullableNumeric() { + var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.NullableNumber))!; + var result = ConfigurationHelper.InferPropertyType(typeof(int?), prop); + + Assert.Equal(PropertyType.Numeric | PropertyType.Nullable, result); + } + + [Fact] + public void InferPropertyType_RecognizesBoolean() { + var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Flag))!; + var result = ConfigurationHelper.InferPropertyType(typeof(bool), prop); + + Assert.Equal(PropertyType.Boolean, result); + } + + [Fact] + public void InferPropertyType_RecognizesDateTime() { + var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Timestamp))!; + var result = ConfigurationHelper.InferPropertyType(typeof(DateTime), prop); + + Assert.Equal(PropertyType.DateTime, result); + } + + [Fact] + public void InferPropertyType_RecognizesDateOnly() { + var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Date))!; + var result = ConfigurationHelper.InferPropertyType(typeof(DateOnly), prop); + + Assert.Equal(PropertyType.DateOnly, result); + } + + [Fact] + public void InferPropertyType_RecognizesTimeOnly() { + var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Time))!; + var result = ConfigurationHelper.InferPropertyType(typeof(TimeOnly), prop); + + Assert.Equal(PropertyType.TimeOnly, result); + } + + [Fact] + public void InferPropertyType_RecognizesEnum() { + var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.EnumValue))!; + var result = ConfigurationHelper.InferPropertyType(typeof(TestEnum), prop); + + Assert.Equal(PropertyType.Enum, result); + } + + [Fact] + public void InferPropertyType_RecognizesList() { + var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Numbers))!; + var result = ConfigurationHelper.InferPropertyType(typeof(List), prop); + + Assert.Equal(PropertyType.Numeric | PropertyType.List, result); + } + + [Fact] + public void InferPropertyType_RecognizesEnumerable() { + var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Strings))!; + var result = ConfigurationHelper.InferPropertyType(typeof(IEnumerable), prop); + + Assert.Equal(PropertyType.Text | PropertyType.List, result); + } + + [Fact] + public void InferPropertyType_RecognizesEmail() { + var prop = typeof(PropertyTypeModel).GetProperty(nameof(PropertyTypeModel.Email))!; + var result = ConfigurationHelper.InferPropertyType(typeof(string), prop); + + Assert.Equal(PropertyType.Email, result); + } } \ No newline at end of file diff --git a/tests/HopFrame.Tests.Core/Repositories/HopFrameRepositoryTests.cs b/tests/HopFrame.Tests.Core/Repositories/HopFrameRepositoryTests.cs index a666aad..8982eab 100644 --- a/tests/HopFrame.Tests.Core/Repositories/HopFrameRepositoryTests.cs +++ b/tests/HopFrame.Tests.Core/Repositories/HopFrameRepositoryTests.cs @@ -4,7 +4,6 @@ using Moq; namespace HopFrame.Tests.Core.Repositories; public class HopFrameRepositoryTests { - private Mock> CreateMock() => new(MockBehavior.Strict); @@ -21,7 +20,7 @@ public class HopFrameRepositoryTests { mock.Setup(r => r.LoadPageAsync(2, 10, It.IsAny())) .ReturnsAsync(expected); - var result = await mock.Object.LoadPageGenericAsync(2, 10); + var result = await mock.Object.LoadPageGenericAsync(2, 10, CancellationToken.None); Assert.Equal(expected, result); } @@ -39,7 +38,7 @@ public class HopFrameRepositoryTests { mock.Setup(r => r.SearchAsync("abc", 1, 20, It.IsAny())) .ReturnsAsync(expected); - var result = await mock.Object.SearchGenericAsync("abc", 1, 20); + var result = await mock.Object.SearchGenericAsync("abc", 1, 20, CancellationToken.None); Assert.Equal(expected, result); } @@ -62,6 +61,24 @@ public class HopFrameRepositoryTests { mock.Verify(r => r.CreateAsync(model, It.IsAny()), Times.Once); } + // ------------------------------------------------------------- + // UpdateGenericAsync + // ------------------------------------------------------------- + + [Fact] + public async Task UpdateGenericAsync_CastsAndDelegates() { + var mock = CreateMock(); + + var model = new TestModel { Id = 77 }; + + mock.Setup(r => r.UpdateAsync(model, It.IsAny())) + .Returns(Task.CompletedTask); + + await mock.Object.UpdateGenericAsync(model, CancellationToken.None); + + mock.Verify(r => r.UpdateAsync(model, It.IsAny()), Times.Once); + } + // ------------------------------------------------------------- // DeleteGenericAsync // ------------------------------------------------------------- diff --git a/tests/HopFrame.Tests.Core/Services/Implementation/ConfigAccessorTests.cs b/tests/HopFrame.Tests.Core/Services/Implementation/ConfigAccessorTests.cs index b2d3f9e..42e2f3d 100644 --- a/tests/HopFrame.Tests.Core/Services/Implementation/ConfigAccessorTests.cs +++ b/tests/HopFrame.Tests.Core/Services/Implementation/ConfigAccessorTests.cs @@ -9,18 +9,23 @@ namespace HopFrame.Tests.Core.Services.Implementation; public class ConfigAccessorTests { private class TestRepository : IHopFrameRepository { - public Task LoadPageGenericAsync(int page, int perPage, CancellationToken ct = default) { + public Task LoadPageGenericAsync(int page, int perPage, CancellationToken ct) { throw new NotImplementedException(); } - public Task CountAsync(CancellationToken ct = default) { + public Task CountAsync(CancellationToken ct) { throw new NotImplementedException(); } - public Task SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct = default) { + public Task SearchGenericAsync(string searchTerm, int page, int perPage, CancellationToken ct) { throw new NotImplementedException(); } public Task CreateGenericAsync(object entry, CancellationToken ct) { throw new NotImplementedException(); } + + public Task UpdateGenericAsync(object entry, CancellationToken ct) { + throw new NotImplementedException(); + } + public Task DeleteGenericAsync(object entry, CancellationToken ct) { throw new NotImplementedException(); } @@ -39,7 +44,8 @@ public class ConfigAccessorTests { Identifier = "Id", DisplayName = "Id", Type = typeof(int), - OrderIndex = 0 + OrderIndex = 0, + PropertyType = PropertyType.Numeric } } };