Commit 46359fe8 authored by Michael Herndon's avatar Michael Herndon

WORL: enhancments to Mettle and testing ef identity

parent 0b7d79ba
......@@ -47,29 +47,10 @@ namespace NerdyMishka.EfCore.Identity
public DbSet<UserToken> UserTokens { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
/*
var o = RelationalOptionsExtension.Extract(optionsBuilder.Options);
if(o != null)
o.WithMigrationsHistoryTableName("identity_migrations_history");
*/
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var configuration = new IdentityEfCoreConfiguration();
configuration.Apply(modelBuilder);
var conventions = new List<IEfCoreConvention>();
conventions.AddRange(new NerdyMishkaConstraintConventions().Conventions);
modelBuilder.ApplyNerdyMishkaConventions(conventions);
}
}
}
\ No newline at end of file
......@@ -5,11 +5,14 @@ using Microsoft.EntityFrameworkCore.Migrations;
using NerdyMishka.EfCore;
using NerdyMishka.EfCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using System;
using System.Collections.Generic;
namespace NerdyMishka.EfCore.Identity
{
public class IdentityEfCoreConfiguration :
ModelConfigurationModule,
IEntityTypeConfiguration<ApiKey>,
IEntityTypeConfiguration<ApiKeyRole>,
IEntityTypeConfiguration<Domain>,
......@@ -31,13 +34,15 @@ namespace NerdyMishka.EfCore.Identity
{
private string schemaName;
private Action<ModelBuilder> seedData;
public IdentityEfCoreConfiguration(string schemaName = "identity")
public IdentityEfCoreConfiguration(string schemaName = "identity", Action<ModelBuilder> seedData = null)
:base(schemaName, seedData)
{
this.schemaName = schemaName;
}
public virtual void Apply(ModelBuilder builder)
protected override void OnModelCreating(ModelBuilder builder)
{
builder.ApplyConfiguration<User>(this);
builder.ApplyConfiguration<Permission>(this);
......@@ -83,6 +88,12 @@ namespace NerdyMishka.EfCore.Identity
builder.Property(o => o.Password)
.HasMaxLength(1024);
}
public virtual IEnumerable<PasswordLogin> SeedPasswordLogin()
{
return Array.Empty<PasswordLogin>();
}
public void Configure(EntityTypeBuilder<ApiKey> builder)
......
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
namespace NerdyMishka.EfCore.Identity
{
public static class DbContextOptionsBuilderExtenions
{
/// <summary>
/// Replaces the Migration Table.
/// </summary>
/// <param name="builder"></param>
/// <param name="migrationTableName"></param>
/// <param name="migrationSchemaName"></param>
/// <typeparam name="TProviderRespository"></typeparam>
/// <typeparam name="TProviderReplacement"></typeparam>
/// <returns></returns>
public static void UseHistoryRespository<TProviderRespository, TProviderReplacement>(
this DbContextOptionsBuilder builder,
string migrationTableName = null,
string migrationSchemaName = null)
where TProviderReplacement: TProviderRespository
where TProviderRespository: IHistoryRepository
{
if(!string.IsNullOrWhiteSpace(migrationTableName))
{
var opts = RelationalOptionsExtension.Extract(builder.Options);
opts.WithMigrationsHistoryTableName(migrationTableName);
if(!string.IsNullOrWhiteSpace(migrationSchemaName))
opts.WithMigrationsHistoryTableSchema(migrationSchemaName);
}
builder.ReplaceService<TProviderRespository, TProviderReplacement>();
}
/// <summary>
/// Hoists the ModelConfiguration to service configration.
/// </summary>
/// <param name="builder"></param>
/// <param name="configure"></param>
/// <typeparam name="T"></typeparam>
public static void UseModelConfiguration<T>(this DbContextOptionsBuilder builder,
Action<ModelBuilder> configure) where T: DbContext
{
var configurationSet = new Microsoft.EntityFrameworkCore.Metadata.Conventions.ConventionSet();
var mb = new ModelBuilder(configurationSet);
if(configure != null)
configure(mb);
builder.UseModel(mb.FinalizeModel());
}
}
}
\ No newline at end of file
......@@ -11,15 +11,24 @@ using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Metadata;
using NerdyMishka.EfCore.Metadata;
namespace NerdyMishka.EfCore.Metadata
namespace NerdyMishka.EfCore
{
public static class NerdyMishkaModelBuilderExtensions
{
public static ModelBuilder ApplyNerdyMishkaNamingConventions(this ModelBuilder builder)
{
var conventions = new List<IEfCoreConvention>();
conventions.AddRange(new NerdyMishkaConstraintConventions().Conventions);
builder.ApplyNerdyMishkaNamingConventions(conventions);
return builder;
}
public static ModelBuilder ApplyNerdyMishkaConventions(
public static ModelBuilder ApplyNerdyMishkaNamingConventions(
this ModelBuilder builder, IEnumerable<IEfCoreConvention> conventions)
{
if(conventions == null || conventions.Count() == 0)
......
using System;
using Microsoft.EntityFrameworkCore;
namespace NerdyMishka.EfCore
{
public class ModelConfigurationModule : IDisposable
{
protected string SchemaName { get; set; }
private Action<ModelBuilder> seedData;
private bool configurationCalled = false;
private bool seedDataCalled = false;
private bool disposed = false;
public ModelConfigurationModule(
string schemaName = null,
Action<ModelBuilder> seedData = null)
{
this.SchemaName = schemaName;
this.seedData = seedData;
}
public virtual void Apply(ModelBuilder builder)
{
this.ApplyConfiguration(builder);
this.ApplySeedData(builder);
}
public virtual void ApplyConfiguration(ModelBuilder builder)
{
if(this.configurationCalled)
return;
this.OnModelCreating(builder);
this.configurationCalled = true;
}
protected virtual void OnModelCreating(ModelBuilder builder)
{
}
public virtual void ApplySeedData(ModelBuilder builder)
{
if(this.seedDataCalled)
return;
if(this.seedData != null)
this.seedData(builder);
this.seedDataCalled = true;
}
public void Dispose()
{
if(this.disposed)
return;
this.Dispose(true);
this.disposed = true;
}
protected virtual void Dispose(bool disposing)
{
}
}
}
\ No newline at end of file
......@@ -25,6 +25,7 @@ namespace NerdyMishka.Extensions.DependencyInjection
if(s_modules.TryGetValue(services, out IList<IModule> set))
set.Clear();
}
}
/// <summary>
......
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
namespace NerdyMishka.EfCore.Identity
{
public static class DbContextOptionsBuilderExtenions
{
/// <summary>
/// Replaces the Migration Table.
/// </summary>
/// <param name="builder"></param>
/// <param name="migrationTableName"></param>
/// <param name="migrationSchemaName"></param>
/// <typeparam name="TProviderRespository"></typeparam>
/// <typeparam name="TProviderReplacement"></typeparam>
/// <returns></returns>
public static void UseHistoryRespository<TProviderRespository, TProviderReplacement>(
this DbContextOptionsBuilder builder,
string migrationTableName = null,
string migrationSchemaName = null)
where TProviderReplacement: TProviderRespository
where TProviderRespository: IHistoryRepository
{
if(!string.IsNullOrWhiteSpace(migrationTableName))
{
var opts = RelationalOptionsExtension.Extract(builder.Options);
opts.WithMigrationsHistoryTableName(migrationTableName);
if(!string.IsNullOrWhiteSpace(migrationSchemaName))
opts.WithMigrationsHistoryTableSchema(migrationSchemaName);
}
builder.ReplaceService<TProviderRespository, TProviderReplacement>();
}
/// <summary>
/// Hoists the ModelConfiguration to service configration.
/// </summary>
/// <param name="builder"></param>
/// <param name="configure"></param>
/// <typeparam name="T"></typeparam>
public static void UseModelConfiguration<T>(this DbContextOptionsBuilder builder,
Action<ModelBuilder> configure) where T: DbContext
{
var configurationSet = new Microsoft.EntityFrameworkCore.Metadata.Conventions.ConventionSet();
var mb = new ModelBuilder(configurationSet);
if(configure != null)
configure(mb);
builder.UseModel(mb.FinalizeModel());
}
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit.Abstractions;
namespace Mettle
{
internal static class Extensions
{
public static void Add<TKey, TValue>(this IDictionary<TKey, List<TValue>> dictionary, TKey key, TValue value)
{
dictionary.GetOrAdd(key).Add(value);
}
public static bool Contains<TKey, TValue>(this IDictionary<TKey, List<TValue>> dictionary, TKey key, TValue value, IEqualityComparer<TValue> valueComparer)
{
List<TValue> values;
if (!dictionary.TryGetValue(key, out values))
return false;
return values.Contains(value, valueComparer);
}
public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key)
where TValue : new()
{
return dictionary.GetOrAdd<TKey, TValue>(key, () => new TValue());
}
public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, Func<TValue> newValue)
{
TValue result;
if (!dictionary.TryGetValue(key, out result))
{
result = newValue();
dictionary[key] = result;
}
return result;
}
public static IEnumerable<IAttributeInfo> GetCustomAttributes(this ITypeInfo typeInfo, Type attributeType)
{
return typeInfo.GetCustomAttributes(attributeType.AssemblyQualifiedName);
}
/// <summary>
/// Gets all the custom attributes for the given assembly.
/// </summary>
/// <param name="assemblyInfo">The assembly</param>
/// <param name="attributeType">The type of the attribute</param>
/// <returns>The matching attributes that decorate the assembly</returns>
public static IEnumerable<IAttributeInfo> GetCustomAttributes(this IAssemblyInfo assemblyInfo, Type attributeType)
{
return assemblyInfo.GetCustomAttributes(attributeType.AssemblyQualifiedName);
}
/// <summary>
/// Gets all the custom attributes for the given attribute.
/// </summary>
/// <param name="attributeInfo">The attribute</param>
/// <param name="attributeType">The type of the attribute to find</param>
/// <returns>The matching attributes that decorate the attribute</returns>
public static IEnumerable<IAttributeInfo> GetCustomAttributes(this IAttributeInfo attributeInfo, Type attributeType)
{
return attributeInfo.GetCustomAttributes(attributeType.AssemblyQualifiedName);
}
/// <summary>
/// Gets all the custom attributes for the method that are of the given type.
/// </summary>
/// <param name="methodInfo">The method</param>
/// <param name="attributeType">The type of the attribute</param>
/// <returns>The matching attributes that decorate the method</returns>
public static IEnumerable<IAttributeInfo> GetCustomAttributes(this IMethodInfo methodInfo, Type attributeType)
{
return methodInfo.GetCustomAttributes(attributeType.AssemblyQualifiedName);
}
}
}
\ No newline at end of file
using Xunit.Abstractions;
using Microsoft.Extensions.Logging;
namespace Mettle
{
public interface ITestLogger
{
ITestOutputHelper Helper { get; }
ILogger Log { get; }
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Mettle
{
/// <summary>
/// The test runner for xUnit.net v2 tests.
/// </summary>
public class MettleTestRunner : TestRunner<IXunitTestCase>
{
readonly IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes;
/// <summary>
/// Initializes a new instance of the <see cref="XunitTestRunner"/> class.
/// </summary>
/// <param name="test">The test that this invocation belongs to.</param>
/// <param name="messageBus">The message bus to report run status to.</param>
/// <param name="testClass">The test class that the test method belongs to.</param>
/// <param name="constructorArguments">The arguments to be passed to the test class constructor.</param>
/// <param name="testMethod">The test method that will be invoked.</param>
/// <param name="testMethodArguments">The arguments to be passed to the test method.</param>
/// <param name="skipReason">The skip reason, if the test is to be skipped.</param>
/// <param name="beforeAfterAttributes">The list of <see cref="BeforeAfterTestAttribute"/>s for this test.</param>
/// <param name="aggregator">The exception aggregator used to run code and collect exceptions.</param>
/// <param name="cancellationTokenSource">The task cancellation token source, used to cancel the test run.</param>
public MettleTestRunner(ITest test,
IMessageBus messageBus,
Type testClass,
object[] constructorArguments,
MethodInfo testMethod,
object[] testMethodArguments,
string skipReason,
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
: base(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, skipReason, aggregator, cancellationTokenSource)
{
this.beforeAfterAttributes = beforeAfterAttributes;
}
/// <summary>
/// Gets the list of <see cref="BeforeAfterTestAttribute"/>s for this test.
/// </summary>
protected IReadOnlyList<BeforeAfterTestAttribute> BeforeAfterAttributes
=> beforeAfterAttributes;
/// <inheritdoc/>
protected override async Task<Tuple<decimal, string>> InvokeTestAsync(ExceptionAggregator aggregator)
{
var output = string.Empty;
ITestLogger testLogger = null;
TestOutputHelper testOutputHelper = null;
foreach(object obj in this.TestCase.TestMethodArguments)
{
testLogger = obj as ITestLogger;
if(testLogger != null)
break;
}
if(testLogger == null)
{
foreach (object obj in ConstructorArguments)
{
testLogger = obj as ITestLogger;
if(testLogger != null)
break;
testOutputHelper = obj as TestOutputHelper;
if (testOutputHelper != null)
break;
}
}
if(testLogger != null)
testOutputHelper = (TestOutputHelper)testLogger.Helper;
if (testOutputHelper != null)
testOutputHelper.Initialize(MessageBus, Test);
var executionTime = await InvokeTestMethodAsync(aggregator);
if (testOutputHelper != null)
{
output = testOutputHelper.Output;
testOutputHelper.Uninitialize();
}
return Tuple.Create(executionTime, output);
}
/// <summary>
/// Override this method to invoke the test method.
/// </summary>
/// <param name="aggregator">The exception aggregator used to run code and collect exceptions.</param>
/// <returns>Returns the execution time (in seconds) spent running the test method.</returns>
protected virtual Task<decimal> InvokeTestMethodAsync(ExceptionAggregator aggregator)
=> new XunitTestInvoker(Test, MessageBus, TestClass, ConstructorArguments, TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource).RunAsync();
}
}
\ No newline at end of file
......@@ -8,7 +8,13 @@
<ItemGroup>
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.assert" Version="2.4.1" />
<PackageReference Include="xunit.assert" Version="2.4.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.XUnit" Version="1.0.21" />
</ItemGroup>
......
using System;
using Microsoft.Extensions.Logging;
using Serilog;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Mettle
{
public class SerilogTestLogger : ITestLogger
{
private string name;
public SerilogTestLogger(
string name = null,
ITestOutputHelper helper = null)
{
this.name = name ?? "";
this.helper = helper ?? new TestOutputHelper();
}
private ITestOutputHelper helper = new TestOutputHelper();
private Microsoft.Extensions.Logging.ILogger logger;
public ITestOutputHelper Helper => helper;
public Serilog.Events.LogEventLevel Level { get; set; } = Serilog.Events.LogEventLevel.Debug;
public Microsoft.Extensions.Logging.ILogger Log
{
get{
if(this.logger == null)
{
this.logger = Microsoft.Extensions.Logging.LoggerFactory.Create((lb) => {
lb.AddSerilog(new LoggerConfiguration()
.MinimumLevel.Verbose()
.WriteTo.TestOutput(this.helper, this.Level)
.CreateLogger());
}).CreateLogger(this.name);
}
return this.logger;
}
}
}
}
\ No newline at end of file
using System;
using System.Collections.Concurrent;
namespace Mettle
{
public class SimpleServiceProvider : IServiceProvider
{
private ConcurrentDictionary<Type, Func<object>> factories =
new ConcurrentDictionary<Type, Func<object>>();
static SimpleServiceProvider()
{
Current = new SimpleServiceProvider();
}
protected SimpleServiceProvider()
{
factories.TryAdd(typeof(IAssert), () => { return AssertImpl.Current; });
factories.TryAdd(typeof(ITestLogger), () => {
return new SerilogTestLogger();
});
}
public static IServiceProvider Current { get; set; }
public void AddSingleton(Type type, object instance)
{
this.factories.TryAdd(type, () => instance);
}
public void AddTransient(Type type)
{
this.factories.TryAdd(type, () => Activator.CreateInstance(type));
}
public void AddTransient(Type type, Func<object> activator)
{
this.factories.TryAdd(type, activator);
}
public object GetService(Type type)
{
if(this.factories.TryGetValue(type, out Func<object> factory))
return factory();
return default;
}
}
}
using Xunit.Abstractions;
using Xunit;
using Serilog;
using Serilog.Events;
using Microsoft.Extensions.Logging;
namespace Mettle
{
public class TestCaseOptions
{
public LogEventLevel Level { get; set; } = LogEventLevel.Verbose;
public IAssert Assert { get; set; } = AssertImpl.Current;
}
public class TestCase
{
protected ILogger Log { get; }
public ILogger Log { get; }
public ITestOutputHelper Output { get; }
protected IAssert Assert { get; }
public IAssert Assert { get; }
public TestCase(
ITestOutputHelper output, TestCaseOptions options = null)
ITestOutputHelper output)
{
options = options ?? new TestCaseOptions();
this.Assert = options.Assert;
this.Log = new LoggerConfiguration()
.MinimumLevel.Verbose()
.WriteTo.TestOutput(output, options.Level)
.CreateLogger()
.ForContext(this.GetType());
var testLogger = new SerilogTestLogger(null, output);
this.Output = testLogger.Helper;
this.Log = testLogger.Log;
this.Assert = AssertImpl.Current;
}
}
}
\ No newline at end of file
using Xunit;
namespace Mettle
{
[System.AttributeUsage(System.AttributeTargets.All, Inherited = false, AllowMultiple = true)]