using System; using System.IO; using System.Linq; using System.Reflection; using AutoMapper; using Examine; using LightInject; using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Components; using Umbraco.Core.Composing; using Umbraco.Core.Composing.CompositionRoots; using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.IO.MediaPathSchemes; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Stubs; using Umbraco.Web; using Umbraco.Web.Services; using Umbraco.Examine; using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web.Composing.CompositionRoots; using Umbraco.Web._Legacy.Actions; using Current = Umbraco.Core.Composing.Current; using Umbraco.Web.Routing; namespace Umbraco.Tests.Testing { /// /// Provides the top-level base class for all Umbraco integration tests. /// /// /// True unit tests do not need to inherit from this class, but most of Umbraco tests /// are not true unit tests but integration tests requiring services, databases, etc. This class /// provides all the necessary environment, through DI. Yes, DI is bad in tests - unit tests. /// But it is OK in integration tests. /// public abstract class UmbracoTestBase { // this class // ensures that Current is properly resetted // ensures that a service container is properly initialized and disposed // compose the required dependencies according to test options (UmbracoTestAttribute) // // everything is virtual (because, why not?) // starting a test runs like this: // - SetUp() // when overriding, call base.SetUp() *first* then setup your own stuff // --- Compose() // when overriding, call base.Commpose() *first* then compose your own stuff // --- Initialize() // same // - test runs // - TearDown() // when overriding, clear you own stuff *then* call base.TearDown() // // about attributes // // this class defines the SetUp and TearDown methods, with proper attributes, and // these attributes are *inherited* so classes inheriting from this class should *not* // add the attributes to SetUp nor TearDown again // // this class is *not* marked with the TestFeature attribute because it is *not* a // test feature, and no test "base" class should be. only actual test feature classes // should be marked with that attribute. protected ServiceContainer Container { get; private set; } protected UmbracoTestAttribute Options { get; private set; } protected static bool FirstTestInSession = true; protected bool FirstTestInFixture = true; internal TestObjects TestObjects { get; private set; } private static TypeLoader _commonTypeLoader; private TypeLoader _featureTypeLoader; #region Accessors protected ILogger Logger => Container.GetInstance(); protected IProfiler Profiler => Container.GetInstance(); protected virtual ProfilingLogger ProfilingLogger => Container.GetInstance(); protected CacheHelper CacheHelper => Container.GetInstance(); protected virtual ISqlSyntaxProvider SqlSyntax => Container.GetInstance(); protected IMapperCollection Mappers => Container.GetInstance(); #endregion #region Setup [SetUp] public virtual void SetUp() { // should not need this if all other tests were clean // but hey, never know, better avoid garbage-in Reset(); Container = new ServiceContainer(); Container.ConfigureUmbracoCore(); TestObjects = new TestObjects(Container); // get/merge the attributes marking the method and/or the classes Options = TestOptionAttributeBase.GetTestOptions(); Compose(); Initialize(); } protected virtual void Compose() { ComposeLogging(Options.Logger); ComposeCacheHelper(); ComposeAutoMapper(Options.AutoMapper); ComposePluginManager(Options.PluginManager); ComposeDatabase(Options.Database); ComposeApplication(Options.WithApplication); // etc ComposeWeb(); ComposeWtf(); // not sure really var composition = new Composition(Container, RuntimeLevel.Run); Compose(composition); } protected virtual void Compose(Composition composition) { } protected virtual void Initialize() { InitializeAutoMapper(Options.AutoMapper); InitializeApplication(Options.WithApplication); } #endregion #region Compose protected virtual void ComposeLogging(UmbracoTestOptions.Logger option) { if (option == UmbracoTestOptions.Logger.Mock) { Container.RegisterSingleton(f => Mock.Of()); Container.RegisterSingleton(f => Mock.Of()); } else if (option == UmbracoTestOptions.Logger.Serilog) { Container.RegisterSingleton(f => new Logger(new FileInfo(TestHelper.MapPathForTest("~/unit-test.config")))); Container.RegisterSingleton(f => new LogProfiler(f.GetInstance())); } Container.RegisterSingleton(f => new ProfilingLogger(f.GetInstance(), f.GetInstance())); } protected virtual void ComposeWeb() { //TODO: Should we 'just' register the WebRuntimeComponent? // imported from TestWithSettingsBase // which was inherited by TestWithApplicationBase so pretty much used everywhere Umbraco.Web.Composing.Current.UmbracoContextAccessor = new TestUmbracoContextAccessor(); // web Container.Register(_ => Umbraco.Web.Composing.Current.UmbracoContextAccessor); Container.RegisterSingleton(); Container.RegisterCollectionBuilder(); Container.Register(); Container.Register(); } protected virtual void ComposeWtf() { // what else? var runtimeStateMock = new Mock(); runtimeStateMock.Setup(x => x.Level).Returns(RuntimeLevel.Run); Container.RegisterSingleton(f => runtimeStateMock.Object); // ah... Container.RegisterCollectionBuilder() .SetProducer(Enumerable.Empty); Container.RegisterCollectionBuilder(); Container.RegisterSingleton(); Container.RegisterSingleton(); } protected virtual void ComposeCacheHelper() { Container.RegisterSingleton(f => CacheHelper.CreateDisabledCacheHelper()); Container.RegisterSingleton(f => f.GetInstance().RuntimeCache); } protected virtual void ComposeAutoMapper(bool configure) { if (configure == false) return; Container.RegisterFrom(); Container.RegisterFrom(); } protected virtual void ComposePluginManager(UmbracoTestOptions.PluginManager pluginManager) { Container.RegisterSingleton(f => { switch (pluginManager) { case UmbracoTestOptions.PluginManager.Default: return _commonTypeLoader ?? (_commonTypeLoader = CreateCommonPluginManager(f)); case UmbracoTestOptions.PluginManager.PerFixture: return _featureTypeLoader ?? (_featureTypeLoader = CreatePluginManager(f)); case UmbracoTestOptions.PluginManager.PerTest: return CreatePluginManager(f); default: throw new ArgumentOutOfRangeException(nameof(pluginManager)); } }); } protected virtual TypeLoader CreatePluginManager(IServiceFactory f) { return CreateCommonPluginManager(f); } private static TypeLoader CreateCommonPluginManager(IServiceFactory f) { return new TypeLoader(f.GetInstance().RuntimeCache, f.GetInstance(), f.GetInstance(), false) { AssembliesToScan = new[] { Assembly.Load("Umbraco.Core"), Assembly.Load("Umbraco.Web"), Assembly.Load("Umbraco.Tests") } }; } protected virtual void ComposeDatabase(UmbracoTestOptions.Database option) { if (option == UmbracoTestOptions.Database.None) return; // create the file // create the schema } protected virtual void ComposeApplication(bool withApplication) { if (withApplication == false) return; var umbracoSettings = SettingsForTests.GetDefaultUmbracoSettings(); var globalSettings = SettingsForTests.GetDefaultGlobalSettings(); //apply these globally SettingsForTests.ConfigureSettings(umbracoSettings); SettingsForTests.ConfigureSettings(globalSettings); // default Datalayer/Repositories/SQL/Database/etc... Container.RegisterFrom(); // register basic stuff that might need to be there for some container resolvers to work Container.RegisterSingleton(factory => umbracoSettings); Container.RegisterSingleton(factory => globalSettings); Container.RegisterSingleton(factory => umbracoSettings.Content); Container.RegisterSingleton(factory => umbracoSettings.Templates); Container.RegisterSingleton(factory => umbracoSettings.WebRouting); Container.Register(factory => new MediaFileSystem(Mock.Of())); Container.RegisterSingleton(factory => ExamineManager.Instance); // replace some stuff Container.RegisterSingleton(factory => Mock.Of(), "ScriptFileSystem"); Container.RegisterSingleton(factory => Mock.Of(), "PartialViewFileSystem"); Container.RegisterSingleton(factory => Mock.Of(), "PartialViewMacroFileSystem"); Container.RegisterSingleton(factory => Mock.Of(), "StylesheetFileSystem"); // need real file systems here as templates content is on-disk only //Container.RegisterSingleton(factory => Mock.Of(), "MasterpageFileSystem"); //Container.RegisterSingleton(factory => Mock.Of(), "ViewFileSystem"); Container.RegisterSingleton(factory => new PhysicalFileSystem("Views", "/views"), "ViewFileSystem"); Container.RegisterSingleton(factory => new PhysicalFileSystem("MasterPages", "/masterpages"), "MasterpageFileSystem"); Container.RegisterSingleton(factory => new PhysicalFileSystem("Xslt", "/xslt"), "XsltFileSystem"); // no factory (noop) Container.RegisterSingleton(); // register application stuff (database factory & context, services...) Container.RegisterCollectionBuilder() .AddCore(); Container.RegisterSingleton(_ => new TransientEventMessagesFactory()); var sqlSyntaxProviders = TestObjects.GetDefaultSqlSyntaxProviders(Logger); Container.RegisterSingleton(_ => sqlSyntaxProviders.OfType().First()); Container.RegisterSingleton(f => new UmbracoDatabaseFactory( Constants.System.UmbracoConnectionName, sqlSyntaxProviders, Logger, Mock.Of())); Container.RegisterSingleton(f => f.TryGetInstance().SqlContext); Container.RegisterCollectionBuilder(); // empty Container.RegisterSingleton(factory => new FileSystems(factory.TryGetInstance())); Container.RegisterSingleton(factory => TestObjects.GetScopeProvider(factory.TryGetInstance(), factory.TryGetInstance(), factory.TryGetInstance())); Container.RegisterSingleton(factory => (IScopeAccessor) factory.GetInstance()); Container.RegisterFrom(); // composition root is doing weird things, fix Container.RegisterSingleton(); Container.RegisterSingleton(); // somehow property editor ends up wanting this Container.RegisterCollectionBuilder(); Container.RegisterSingleton(); // note - don't register collections, use builders Container.RegisterCollectionBuilder(); Container.RegisterSingleton(); Container.RegisterSingleton(); } #endregion #region Initialize protected virtual void InitializeAutoMapper(bool configure) { if (configure == false) return; Mapper.Initialize(configuration => { var profiles = Container.GetAllInstances(); foreach (var profile in profiles) configuration.AddProfile(profile); }); } protected virtual void InitializeApplication(bool withApplication) { if (withApplication == false) return; TestHelper.InitializeContentDirectories(); } #endregion #region TearDown and Reset [TearDown] public virtual void TearDown() { FirstTestInFixture = false; FirstTestInSession = false; Reset(); if (Options.WithApplication) { TestHelper.CleanContentDirectories(); TestHelper.CleanUmbracoSettingsConfig(); } } protected virtual void Reset() { // reset and dispose scopes // ensures we don't leak an opened database connection // which would lock eg SqlCe .sdf files if (Container?.TryGetInstance() is ScopeProvider scopeProvider) { Core.Scoping.Scope scope; while ((scope = scopeProvider.AmbientScope) != null) { scope.Reset(); scope.Dispose(); } } Current.Reset(); Container?.Dispose(); Container = null; // reset all other static things that should not be static ;( UriUtility.ResetAppDomainAppVirtualPath(); SettingsForTests.Reset(); // fixme - should it be optional? Mapper.Reset(); // clear static events DocumentRepository.ClearScopeEvents(); MediaRepository.ClearScopeEvents(); MemberRepository.ClearScopeEvents(); ContentTypeService.ClearScopeEvents(); MediaTypeService.ClearScopeEvents(); MemberTypeService.ClearScopeEvents(); } #endregion } }