using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Reflection; using System.Threading; using System.Xml.Linq; using Examine; using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.IO.MediaPathSchemes; using Umbraco.Core.Logging; using Umbraco.Core.Logging.Serilog; using Umbraco.Core.Manifest; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; using Umbraco.Tests.Components; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Stubs; using Umbraco.Web; using Umbraco.Web.Services; using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web.Actions; using Umbraco.Web.ContentApps; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; using Umbraco.Web.Trees; using Umbraco.Core.Composing.CompositionExtensions; using Umbraco.Core.Hosting; using Umbraco.Core.Mapping; using Umbraco.Core.Serialization; using Umbraco.Web.Composing.CompositionExtensions; using Umbraco.Web.Hosting; using Umbraco.Web.Sections; using Current = Umbraco.Core.Composing.Current; using FileSystems = Umbraco.Core.IO.FileSystems; using Umbraco.Web.Templates; using Umbraco.Web.PropertyEditors; using Umbraco.Core.Dictionary; using Umbraco.Core.Models.Identity; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Net; using Umbraco.Web.Security; 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 Composition Composition { get; private set; } protected IFactory Factory { 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 => Factory.GetInstance(); protected IIOHelper IOHelper { get; private set; } protected IDataTypeService DataTypeService => Factory.GetInstance(); protected IPasswordHasher PasswordHasher => Factory.GetInstance(); protected Lazy PropertyEditorCollection => new Lazy(() => Factory.GetInstance()); protected ILocalizationService LocalizationService => Factory.GetInstance(); protected ILocalizedTextService LocalizedTextService { get; private set; } protected IShortStringHelper ShortStringHelper => Factory?.GetInstance() ?? TestHelper.ShortStringHelper; protected IUmbracoVersion UmbracoVersion { get; private set; } protected ITypeFinder TypeFinder { get; private set; } protected IProfiler Profiler => Factory.GetInstance(); protected virtual IProfilingLogger ProfilingLogger => Factory.GetInstance(); protected IHostingEnvironment HostingEnvironment => Factory.GetInstance(); protected IIpResolver IpResolver => Factory.GetInstance(); protected IBackOfficeInfo BackOfficeInfo => Factory.GetInstance(); protected AppCaches AppCaches => Factory.GetInstance(); protected virtual ISqlSyntaxProvider SqlSyntax => Factory.GetInstance(); protected IMapperCollection Mappers => Factory.GetInstance(); protected UmbracoMapper Mapper => Factory.GetInstance(); protected IRuntimeState RuntimeState => ComponentTests.MockRuntimeState(RuntimeLevel.Run); #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(); // get/merge the attributes marking the method and/or the classes Options = TestOptionAttributeBase.GetTestOptions(); // FIXME: align to runtimes & components - don't redo everything here !!!! Yes this is getting painful var (logger, profiler) = GetLoggers(Options.Logger); var proflogger = new ProfilingLogger(logger, profiler); IOHelper = TestHelper.IOHelper; TypeFinder = new TypeFinder(logger); var appCaches = GetAppCaches(); var globalSettings = SettingsForTests.GetDefaultGlobalSettings(); var hostingSettings = SettingsForTests.GetDefaultHostingSettings(); var settings = SettingsForTests.GetDefaultUmbracoSettings(); IHostingEnvironment hostingEnvironment = new AspNetHostingEnvironment(hostingSettings); IBackOfficeInfo backOfficeInfo = new AspNetBackOfficeInfo(globalSettings, IOHelper, settings, logger); IIpResolver ipResolver = new AspNetIpResolver(); UmbracoVersion = new UmbracoVersion(globalSettings); LocalizedTextService = new LocalizedTextService(new Dictionary>(), logger); var typeLoader = GetTypeLoader(IOHelper, TypeFinder, appCaches.RuntimeCache, hostingEnvironment, proflogger, Options.TypeLoader); var register = TestHelper.GetRegister(); Composition = new Composition(register, typeLoader, proflogger, ComponentTests.MockRuntimeState(RuntimeLevel.Run), TestHelper.GetConfigs(), TestHelper.IOHelper, AppCaches.NoCache); Composition.RegisterUnique(IOHelper); Composition.RegisterUnique(UmbracoVersion); Composition.RegisterUnique(TypeFinder); Composition.RegisterUnique(LocalizedTextService); Composition.RegisterUnique(typeLoader); Composition.RegisterUnique(logger); Composition.RegisterUnique(profiler); Composition.RegisterUnique(proflogger); Composition.RegisterUnique(appCaches); Composition.RegisterUnique(hostingEnvironment); Composition.RegisterUnique(backOfficeInfo); Composition.RegisterUnique(ipResolver); Composition.RegisterUnique(); Composition.RegisterUnique(); Composition.RegisterUnique(TestHelper.ShortStringHelper); TestObjects = new TestObjects(register); Compose(); Current.Factory = Factory = Composition.CreateFactory(); Initialize(); } protected virtual void Compose() { ComposeMapper(Options.Mapper); ComposeDatabase(Options.Database); ComposeApplication(Options.WithApplication); // etc ComposeWeb(); ComposeMisc(); // not sure really Compose(Composition); } protected virtual void Compose(Composition composition) { } protected virtual void Initialize() { InitializeApplication(Options.WithApplication); } #endregion #region Compose protected virtual (ILogger, IProfiler) GetLoggers(UmbracoTestOptions.Logger option) { ILogger logger; IProfiler profiler; switch (option) { case UmbracoTestOptions.Logger.Mock: logger = Mock.Of(); profiler = Mock.Of(); break; case UmbracoTestOptions.Logger.Serilog: logger = new SerilogLogger(TestHelper.CoreDebug, IOHelper, TestHelper.Marchal, new FileInfo(TestHelper.MapPathForTest("~/unit-test.config"))); profiler = new LogProfiler(logger); break; case UmbracoTestOptions.Logger.Console: logger = new ConsoleLogger(new MessageTemplates()); profiler = new LogProfiler(logger); break; default: throw new NotSupportedException($"Logger option {option} is not supported."); } return (logger, profiler); } protected virtual AppCaches GetAppCaches() { return AppCaches.Disabled; } protected virtual void ComposeWeb() { // imported from TestWithSettingsBase // which was inherited by TestWithApplicationBase so pretty much used everywhere Umbraco.Web.Composing.Current.UmbracoContextAccessor = new TestUmbracoContextAccessor(); // web Composition.RegisterUnique(_ => Umbraco.Web.Composing.Current.UmbracoContextAccessor); Composition.RegisterUnique(); Composition.WithCollectionBuilder(); Composition.DataValueReferenceFactories(); Composition.RegisterUnique(); Composition.RegisterUnique(); Composition.RegisterUnique(); Composition.SetCultureDictionaryFactory(); Composition.Register(f => f.GetInstance().CreateDictionary(), Lifetime.Singleton); // register back office sections in the order we want them rendered Composition.WithCollectionBuilder().Append() .Append() .Append() .Append() .Append() .Append() .Append() .Append(); Composition.RegisterUnique(); Composition.RegisterUnique(); Composition.RegisterUnique(); Composition.RegisterUnique(); Composition.RegisterUnique(); } protected virtual void ComposeMisc() { // what else? var runtimeStateMock = new Mock(); runtimeStateMock.Setup(x => x.Level).Returns(RuntimeLevel.Run); Composition.RegisterUnique(f => runtimeStateMock.Object); // ah... Composition.WithCollectionBuilder(); Composition.WithCollectionBuilder(); Composition.RegisterUnique(); Composition.RegisterUnique(); Composition.RegisterUnique(); // register empty content apps collection Composition.WithCollectionBuilder(); // manifest Composition.ManifestValueValidators(); Composition.ManifestFilters(); } protected virtual void ComposeMapper(bool configure) { if (configure == false) return; Composition .ComposeCoreMappingProfiles() .ComposeWebMappingProfiles(); } protected virtual TypeLoader GetTypeLoader(IIOHelper ioHelper, ITypeFinder typeFinder, IAppPolicyCache runtimeCache, IHostingEnvironment hostingEnvironment, IProfilingLogger logger, UmbracoTestOptions.TypeLoader option) { switch (option) { case UmbracoTestOptions.TypeLoader.Default: return _commonTypeLoader ?? (_commonTypeLoader = CreateCommonTypeLoader(ioHelper, typeFinder, runtimeCache, logger, hostingEnvironment)); case UmbracoTestOptions.TypeLoader.PerFixture: return _featureTypeLoader ?? (_featureTypeLoader = CreateTypeLoader(ioHelper, typeFinder, runtimeCache, logger, hostingEnvironment)); case UmbracoTestOptions.TypeLoader.PerTest: return CreateTypeLoader(ioHelper, typeFinder, runtimeCache, logger, hostingEnvironment); default: throw new ArgumentOutOfRangeException(nameof(option)); } } protected virtual TypeLoader CreateTypeLoader(IIOHelper ioHelper, ITypeFinder typeFinder, IAppPolicyCache runtimeCache, IProfilingLogger logger, IHostingEnvironment hostingEnvironment) { return CreateCommonTypeLoader(ioHelper, typeFinder, runtimeCache, logger, hostingEnvironment); } // common to all tests = cannot be overriden private static TypeLoader CreateCommonTypeLoader(IIOHelper ioHelper, ITypeFinder typeFinder, IAppPolicyCache runtimeCache, IProfilingLogger logger, IHostingEnvironment hostingEnvironment) { return new TypeLoader(ioHelper, typeFinder, runtimeCache, new DirectoryInfo(hostingEnvironment.LocalTempPath), logger, false, new[] { Assembly.Load("Umbraco.Core"), Assembly.Load("Umbraco.Web"), Assembly.Load("Umbraco.Tests"), Assembly.Load("Umbraco.Infrastructure") }); } protected virtual void ComposeDatabase(UmbracoTestOptions.Database option) { if (option == UmbracoTestOptions.Database.None) return; // create the file // create the schema } protected virtual void ComposeSettings() { Composition.Configs.Add(SettingsForTests.GetDefaultUmbracoSettings); Composition.Configs.Add(SettingsForTests.GetDefaultGlobalSettings); Composition.Configs.Add(SettingsForTests.GetDefaultHostingSettings); //Composition.Configs.Add(() => new DefaultUserPasswordConfig()); } protected virtual void ComposeApplication(bool withApplication) { ComposeSettings(); if (withApplication == false) return; // default Datalayer/Repositories/SQL/Database/etc... Composition.ComposeRepositories(); // register basic stuff that might need to be there for some container resolvers to work Composition.RegisterUnique(factory => factory.GetInstance().Content); Composition.RegisterUnique(factory => factory.GetInstance().WebRouting); Composition.RegisterUnique(factory => ExamineManager.Instance); Composition.RegisterUnique(); // register filesystems Composition.RegisterUnique(factory => TestObjects.GetFileSystemsMock()); var logger = Mock.Of(); var scheme = Mock.Of(); var mediaFileSystem = new MediaFileSystem(Mock.Of(), scheme, logger, TestHelper.ShortStringHelper); Composition.RegisterUnique(factory => mediaFileSystem); // no factory (noop) Composition.RegisterUnique(); // register application stuff (database factory & context, services...) Composition.WithCollectionBuilder() .AddCoreMappers(); Composition.RegisterUnique(_ => new TransientEventMessagesFactory()); Composition.RegisterUnique(f => new UmbracoDatabaseFactory( Constants.System.UmbracoConnectionName, Logger, new Lazy(f.GetInstance), TestHelper.GetConfigs(), TestHelper.DbProviderFactoryCreator, TestHelper.BulkSqlInsertProvider)); Composition.RegisterUnique(f => f.TryGetInstance().SqlContext); Composition.WithCollectionBuilder(); // empty Composition.RegisterUnique(factory => TestObjects.GetScopeProvider(factory.TryGetInstance(), factory.TryGetInstance(), factory.TryGetInstance(), factory.TryGetInstance())); Composition.RegisterUnique(factory => (IScopeAccessor) factory.GetInstance()); Composition.ComposeServices(); // composition root is doing weird things, fix Composition.RegisterUnique(); Composition.RegisterUnique(); // somehow property editor ends up wanting this Composition.WithCollectionBuilder(); Composition.RegisterUnique(); // note - don't register collections, use builders Composition.WithCollectionBuilder(); Composition.RegisterUnique(); Composition.RegisterUnique(); } #endregion #region Initialize 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 (Factory?.TryGetInstance() is ScopeProvider scopeProvider) { Scope scope; while ((scope = scopeProvider.AmbientScope) != null) { scope.Reset(); scope.Dispose(); } } Current.Reset(); // disposes the factory // reset all other static things that should not be static ;( UriUtility.ResetAppDomainAppVirtualPath(); SettingsForTests.Reset(); // FIXME: should it be optional? // clear static events DocumentRepository.ClearScopeEvents(); MediaRepository.ClearScopeEvents(); MemberRepository.ClearScopeEvents(); ContentTypeService.ClearScopeEvents(); MediaTypeService.ClearScopeEvents(); MemberTypeService.ClearScopeEvents(); } #endregion } }