diff --git a/src/XrmMockup365/Core.cs b/src/XrmMockup365/Core.cs index 86b99dc0..23188561 100644 --- a/src/XrmMockup365/Core.cs +++ b/src/XrmMockup365/Core.cs @@ -1361,7 +1361,7 @@ internal async System.Threading.Tasks.Task ExecuteFormulaFields(EntityMetadata e continue; } - entity[attr.LogicalName] = await FormulaFieldEvaluator.Evaluate(definition, entity); + entity[attr.LogicalName] = await FormulaFieldEvaluator.Evaluate(definition, entity, TimeOffset); } } diff --git a/src/XrmMockup365/CustomFunction/ISOWeekNumFunction.cs b/src/XrmMockup365/CustomFunction/ISOWeekNumFunction.cs new file mode 100644 index 00000000..49573fae --- /dev/null +++ b/src/XrmMockup365/CustomFunction/ISOWeekNumFunction.cs @@ -0,0 +1,45 @@ +using Microsoft.PowerFx; +using Microsoft.PowerFx.Interpreter; +using Microsoft.PowerFx.Types; +using System; +using System.Globalization; + +namespace DG.Tools.XrmMockup.CustomFunction +{ + internal class ISOWeekNumFunction : ReflectionFunction + { + private readonly TimeSpan timeOffset; + + public ISOWeekNumFunction(TimeSpan timeOffset) + : base("ISOWeekNum", FormulaType.Decimal, FormulaType.DateTime) + { + this.timeOffset = timeOffset; + } + public DecimalValue Execute(DateTimeValue date) + { + var utcDate = date?.GetConvertedValue(TimeZoneInfo.Utc); + if (utcDate == null || utcDate.Value <= DateTime.MinValue) + throw new CustomFunctionErrorException("Invalid date or time value", ErrorKind.InvalidArgument); + + utcDate = utcDate.Value.Add(timeOffset); + +#if DATAVERSE_SERVICE_CLIENT + var weekNumber = ISOWeek.GetWeekOfYear(utcDate.Value); +#else + // .NET Framework does not have ISOWeek class + // Implementing ISO 8601 week date algorithm + // https://learn.microsoft.com/en-us/archive/blogs/shawnste/iso-8601-week-of-year-format-in-microsoft-net + + var dayOfWeek = utcDate.Value.DayOfWeek; + if (dayOfWeek >= DayOfWeek.Monday && dayOfWeek <= DayOfWeek.Wednesday) + { + utcDate = utcDate.Value.AddDays(3); + } + + var weekNumber = CultureInfo.InvariantCulture.Calendar.GetWeekOfYear(utcDate.Value, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday); +#endif + + return FormulaValue.New(weekNumber); + } + } +} diff --git a/src/XrmMockup365/CustomFunction/IsUTCTodayFunction.cs b/src/XrmMockup365/CustomFunction/IsUTCTodayFunction.cs new file mode 100644 index 00000000..b59dff94 --- /dev/null +++ b/src/XrmMockup365/CustomFunction/IsUTCTodayFunction.cs @@ -0,0 +1,27 @@ +using Microsoft.PowerFx; +using Microsoft.PowerFx.Interpreter; +using Microsoft.PowerFx.Types; +using System; + +namespace DG.Tools.XrmMockup.CustomFunction +{ + internal class IsUTCTodayFunction : ReflectionFunction + { + private readonly TimeSpan timeOffset; + + public IsUTCTodayFunction(TimeSpan timeOffset) + : base("IsUTCToday", FormulaType.Boolean, FormulaType.DateTime) + { + this.timeOffset = timeOffset; + } + public BooleanValue Execute(DateTimeValue date) + { + var utcDate = date?.GetConvertedValue(TimeZoneInfo.Utc); + if (utcDate == null || utcDate <= DateTime.MinValue) + throw new CustomFunctionErrorException("Invalid date or time value", ErrorKind.InvalidArgument); + + var utcToday = DateTime.UtcNow.Add(timeOffset).Date; + return FormulaValue.New(utcDate.Value.Date == utcToday); + } + } +} diff --git a/src/XrmMockup365/CustomFunction/UTCNowFunction.cs b/src/XrmMockup365/CustomFunction/UTCNowFunction.cs new file mode 100644 index 00000000..8a7d4ffe --- /dev/null +++ b/src/XrmMockup365/CustomFunction/UTCNowFunction.cs @@ -0,0 +1,22 @@ +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; +using System; + +namespace DG.Tools.XrmMockup.CustomFunction +{ + internal class UTCNowFunction : ReflectionFunction + { + private readonly TimeSpan timeOffset; + + public UTCNowFunction(TimeSpan timeOffset) + : base("UTCNow", FormulaType.DateTime) + { + this.timeOffset = timeOffset; + } + public DateTimeValue Execute() + { + var utcNow = DateTime.UtcNow.Add(timeOffset); + return FormulaValue.New(utcNow); + } + } +} diff --git a/src/XrmMockup365/CustomFunction/UTCTodayFunction.cs b/src/XrmMockup365/CustomFunction/UTCTodayFunction.cs new file mode 100644 index 00000000..ba8ca3e2 --- /dev/null +++ b/src/XrmMockup365/CustomFunction/UTCTodayFunction.cs @@ -0,0 +1,23 @@ +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; +using System; + +namespace DG.Tools.XrmMockup.CustomFunction +{ + internal class UTCTodayFunction : ReflectionFunction + { + private readonly TimeSpan timeOffset; + + public UTCTodayFunction(TimeSpan timeOffset) + : base("UTCToday", FormulaType.DateTime) + { + this.timeOffset = timeOffset; + } + + public DateTimeValue Execute() + { + var utcNow = DateTime.UtcNow.Add(timeOffset); + return FormulaValue.New(utcNow.Date); + } + } +} diff --git a/src/XrmMockup365/FormulaFieldEvaluator.cs b/src/XrmMockup365/FormulaFieldEvaluator.cs index 3dafe4f9..a6781a8d 100644 --- a/src/XrmMockup365/FormulaFieldEvaluator.cs +++ b/src/XrmMockup365/FormulaFieldEvaluator.cs @@ -1,9 +1,12 @@ -using Microsoft.PowerFx.Dataverse; +using DG.Tools.XrmMockup.CustomFunction; using Microsoft.PowerFx; +using Microsoft.PowerFx.Dataverse; using Microsoft.Xrm.Sdk; -using System.Threading.Tasks; -using System.Threading; +using System; +using System.Collections.Generic; using System.Globalization; +using System.Threading; +using System.Threading.Tasks; namespace DG.Tools.XrmMockup { @@ -18,11 +21,18 @@ public FormulaFieldEvaluator(IOrganizationServiceFactory serviceFactory) _dataverseConnection = SingleOrgPolicy.New(_organizationService); } - public async Task Evaluate(string formula, Entity thisEntity) + public async Task Evaluate(string formula, Entity thisEntity, TimeSpan timeOffset) { var rowScopeSymbols = _dataverseConnection.GetRowScopeSymbols(thisEntity.LogicalName, true); - var engine = new RecalcEngine(); + var config = new PowerFxConfig(); + config.AddFunction(new UTCTodayFunction(timeOffset)); + config.AddFunction(new UTCNowFunction(timeOffset)); + config.AddFunction(new IsUTCTodayFunction(timeOffset)); + config.AddFunction(new ISOWeekNumFunction(timeOffset)); + + var engine = new RecalcEngine(config); + var combinedSymbols = ReadOnlySymbolTable.Compose(rowScopeSymbols, _dataverseConnection.Symbols); var checkResult = engine.Check(formula, new ParserOptions(CultureInfo.InvariantCulture), combinedSymbols); checkResult.ThrowOnErrors(); diff --git a/tests/XrmMockup365Test/TestFormulaFields.cs b/tests/XrmMockup365Test/TestFormulaFields.cs index f549ae4d..577f316c 100644 --- a/tests/XrmMockup365Test/TestFormulaFields.cs +++ b/tests/XrmMockup365Test/TestFormulaFields.cs @@ -1,14 +1,23 @@ using DG.Tools.XrmMockup; using DG.XrmFramework.BusinessDomain.ServiceContext; +using Microsoft.PowerFx.Types; using Microsoft.Xrm.Sdk; using System; +using System.Globalization; +using System.Linq; using Xunit; +using TTask = System.Threading.Tasks.Task; + namespace DG.XrmMockupTest { - public class TestFormulaFields : UnitTestBase, IOrganizationServiceFactory + public class TestFormulaFields : UnitTestBase { - public TestFormulaFields(XrmMockupFixture fixture) : base(fixture) { } + private readonly IOrganizationServiceFactory serviceFactory; + + public TestFormulaFields(XrmMockupFixture fixture) : base(fixture) { + serviceFactory = new UnitTestOrganizationServiceFactory(this); + } [Theory] [InlineData("123", "1 + 1", "2")] @@ -16,73 +25,378 @@ public TestFormulaFields(XrmMockupFixture fixture) : base(fixture) { } [InlineData("123", "Concatenate(name, \" - \", Text(accountnumber))", "Test - 123")] [InlineData("123", "If(Decimal(accountnumber) > 0, name & \" - \" & accountnumber, \"No Account Number\")", "Test - 123")] [InlineData("0", "If(Decimal(accountnumber) > 0, name & \" - \" & accountnumber, \"No Account Number\")", "No Account Number")] - public async System.Threading.Tasks.Task CanEvaluateExpressionOnEntity(string accountNumber, string formula, string expected) + public async TTask CanEvaluateExpressionOnEntity(string accountNumber, string formula, string expected) { - var evaluator = new FormulaFieldEvaluator(this); + var evaluator = new FormulaFieldEvaluator(serviceFactory); var account = Create(new Account { Name = "Test", AccountNumber = accountNumber }); - var result = await evaluator.Evaluate(formula, account); + var result = await evaluator.Evaluate(formula, account, TimeSpan.Zero); Assert.Equal(expected, result.ToString()); } [Fact] - public async System.Threading.Tasks.Task CanEvaluateExpressionWithRelatedEntity() + public async TTask CanEvaluateExpressionWithRelatedEntity() { - var evaluator = new FormulaFieldEvaluator(this); + var evaluator = new FormulaFieldEvaluator(serviceFactory); var adminUserId = Guid.Parse("3b961284-cd7a-4fa3-af7e-89802e88dd5c"); var animal = Create(new dg_animal { dg_name = "Fluffy", OwnerId = new EntityReference(SystemUser.EntityLogicalName, adminUserId) }); var animalFood = Create(new dg_animalfood { dg_AnimalId = new EntityReference(dg_animal.EntityLogicalName, animal.Id), dg_name = "Meatballs" }); - var result = await evaluator.Evaluate("\"Test: \" & dg_animalowner", animal); + var result = await evaluator.Evaluate("\"Test: \" & dg_animalowner", animal, TimeSpan.Zero); Assert.Equal("Test: Fluffy is a very good animal, and Admin loves them very much", result); - result = await evaluator.Evaluate("If(dg_AnimalId.dg_name = \"Fluffy\", \"Test: \" & dg_AnimalId.dg_name & \" eats \" & dg_name, \"New telegraph, who dis?\")", animalFood); + result = await evaluator.Evaluate("If(dg_AnimalId.dg_name = \"Fluffy\", \"Test: \" & dg_AnimalId.dg_name & \" eats \" & dg_name, \"New telegraph, who dis?\")", animalFood, TimeSpan.Zero); Assert.Equal("Test: Fluffy eats Meatballs", result); } - [Fact(Skip = "Function not implemented in Eval yet")] - public async System.Threading.Tasks.Task CanEvaluateExpressionWithUtcToday() + [Theory] + [ClassData(typeof(FunctionCases))] + public async TTask CanEvaluateFunction(string formula, object expected) { - var evaluator = new FormulaFieldEvaluator(this); + var evaluator = new FormulaFieldEvaluator(serviceFactory); + var result = await evaluator.Evaluate(formula, new Account(), TimeSpan.Zero); - var result = await evaluator.Evaluate("UTCToday()", new dg_animal()); - Assert.NotNull(result); + if (result is ErrorValue error && expected is string errorString) + { + var errorMessage = string.Join("\n", error.Errors.Select(e => e.Message)); + Assert.Contains(errorString, errorMessage); + } + else if (result is DateTime resultDateTime) + { + var expectedDateTime = (expected is DateTime dt) + ? dt + : (expected is "DateTime.Now") + ? DateTime.Now + : throw new InvalidOperationException("Expected value is not a DateTime or 'DateTime.Now'"); + Assert.Equal(expectedDateTime, resultDateTime, TimeSpan.FromSeconds(1)); + } + else + { + Assert.Equal(expected, result); + } } - private TEntity Create(TEntity entity) where TEntity : Entity + [Theory] + [ClassData(typeof(TextFunctionCases))] + public async TTask CanEvaluateTextFunction(string formula, string expected) { - var id = orgAdminService.Create(entity); + var evaluator = new FormulaFieldEvaluator(serviceFactory); + var result = await evaluator.Evaluate(formula, new Account(), TimeSpan.Zero); - return orgAdminService.Retrieve(entity.LogicalName, id, new Microsoft.Xrm.Sdk.Query.ColumnSet(true)).ToEntity(); + Assert.Equal(expected, result); } - public IOrganizationService CreateOrganizationService(Guid? userId) + [Theory] + [InlineData("ISOWeekNum(Date(2025, 1, 1))", 1)] // Returns the ISO week number of a date/time value. + [InlineData("IsUTCToday(DateTime(2025, 1, 1, 12, 30, 40))", false)] // Checks whether a date/time value is sometime today in Coordinated Universal Time (UTC). + [InlineData("IsUTCToday(Now())", true)] + [InlineData("UTCNow()", "DateTime.UtcNow")] // Returns the current date/time value in Coordinated Universal Time (UTC). + [InlineData("UTCToday()", "DateTime.UtcNow.Date")] // Returns the current date-only value in Coordinated Universal Time (UTC). + public async TTask CanEvaluateDataverseDateTimeCustomFunctions(string formula, object expected) { - if (userId == null || userId == Guid.Empty) - { - return orgAdminService; - } - else if (userId == testUser1.Id) - { - return testUser1Service; - } - else if (userId == testUser2.Id) - { - return testUser2Service; - } - else if (userId == testUser3.Id) + // Verify that the DataTime custom functions work as expected. + // https://learn.microsoft.com/en-us/power-platform/power-fx/formula-reference-formula-columns + + var evaluator = new FormulaFieldEvaluator(serviceFactory); + var result = await evaluator.Evaluate(formula, new Account(), TimeSpan.Zero); + + if (result is DateTime resultDateTime) { - return testUser3Service; + DateTime? expectedDateTime; + if (expected is "DateTime.UtcNow") + { + expectedDateTime = DateTime.UtcNow; + } + else if (expected is "DateTime.UtcNow.Date") + { + expectedDateTime = DateTime.UtcNow.Date; + } + else if (expected is DateTime dt) + { + expectedDateTime = dt; + } + else + { + throw new Exception("Expected value is not a DateTime or 'DateTime.UtcNow'/'DateTime.UtcNow.Date'"); + } + + Assert.Equal(expectedDateTime.Value, resultDateTime, TimeSpan.FromSeconds(1)); } - else if (userId == testUser4.Id) + else if (result is decimal resultDecimal && expected is int expectedInt) { - return testUser4Service; + Assert.Equal(expectedInt, resultDecimal); } else { - throw new ArgumentException($"Unknown userId: {userId}"); + Assert.Equal(expected, result); } } + + [Fact] + public async TTask UTCNowRespectsOffset() + { + var evaluator = new FormulaFieldEvaluator(serviceFactory); + var result = await evaluator.Evaluate("UTCNow()", new Account(), TimeSpan.FromDays(1)); + var resultDateTime = (DateTime)result; + Assert.Equal(DateTime.UtcNow.AddDays(1), resultDateTime, TimeSpan.FromSeconds(1)); + } + + [Fact] + public async TTask UTCTodayRespectsOffset() + { + var evaluator = new FormulaFieldEvaluator(serviceFactory); + var result = await evaluator.Evaluate("UTCToday()", new Account(), TimeSpan.FromDays(1)); + var resultDateTime = (DateTime)result; + Assert.Equal(DateTime.UtcNow.Date.AddDays(1), resultDateTime, TimeSpan.FromSeconds(1)); + } + + [Fact] + public async TTask IsUTCTodayRespectsOffset() + { + var evaluator = new FormulaFieldEvaluator(serviceFactory); + var result = await evaluator.Evaluate("IsUTCToday(UTCNow())", new Account(), TimeSpan.FromDays(1)); + Assert.Equal(true, result); + } + } + + public class FunctionCases : TheoryData + { + public FunctionCases() + { + // Absolute value of a number. + Add("Abs(-10.5)", 10.5m); + + // Boolean logic AND. Returns true if all arguments are true. You can also use the && operator. + Add("And(true, true)", true); + Add("And(true, false)", false); + Add("And(false, false)", false); + Add("And(false, true)", false); + Add("true && true", true); + + // Calculates the average of a table expression or a set of arguments. + Add("Average(2, 6)", 4m); + + // Returns a blank value that can be used to insert a NULL value in a data source. + Add("Blank()", null); + + // Translates a character code into a string. + Add("Char(42)", "*"); + + // Concatenates strings. + Add("Concatenate(\"Hello,\", \" \", \"World!\")", "Hello, World!"); + + // Adds days, months, quarters, or years to a date/time value. + Add("DateAdd(Date(2025, 1, 1), 5, TimeUnit.Days)", new DateTime(2025, 1, 6)); + Add("DateAdd(Date(2025, 1, 1), 1, TimeUnit.Months)", new DateTime(2025, 2, 1)); + Add("DateAdd(Date(2025, 1, 1), 1, TimeUnit.Quarters)", new DateTime(2025, 4, 1)); + Add("DateAdd(Date(2025, 1, 1), 1, TimeUnit.Years)", new DateTime(2026, 1, 1)); + + // Subtracts two date values, and shows the result in days, months, quarters, or years. + Add("DateDiff(Date(2025, 2, 1), Date(2025, 1, 1), TimeUnit.Months)", -1m); + + // Retrieves the day portion of a date/time value. + Add("Day(Date(2025, 1, 5))", 5m); + + // Converts a string to a decimal number. + Add("Decimal(\"10.5\")", 10.5m); + + // Checks whether a text string ends with another text string. + Add("EndsWith(\"some string\", \"string\")", true); + Add("EndsWith(\"some string\", \"nooooo\")", false); + + // Returns e raised to a power. + Add("Exp(0)", 1d); + + // Converts a string to a floating point number. + Add("Float(\"10.5\")", 10.5d); + + // Returns the hour portion of a date/time value. + Add("Hour(DateTime(2025, 1, 1, 12, 30, 40))", 12m); + + // Returns one value if a condition is true and another value if not. + Add("If(true, \"Yes\", \"No\")", "Yes"); + Add("If(false, \"Yes\", \"No\")", "No"); + + // Detects errors and provides an alternative value or takes action. + Add("IfError(Error(\"FAULT\"), \"Caught!\")", "Caught!"); + + // Rounds down to the nearest integer. + Add("Int(10.5)", 10m); + + // Checks for a blank value. + Add("IsBlank(Blank())", true); + Add("IsBlank(\"Not blank\")", false); + + // Error() : Create a custom error or pass through an error. + // IsError() : Checks for an error. + Add("IsError(Error(\"FAULT\"))", true); + Add("IsError(\"No error\")", false); + + // Returns the left-most portion of a string. + Add("Left(\"Hello, world!\", 5)", "Hello"); + + // Returns the length of a string. + Add("Len(\"Hello, world!\")", 13m); + + // Returns the natural log. + Add("Ln(Exp(1))", 1d); + Add("Ln(1)", 0d); + Add("Ln(0)", "The function 'Ln' returned a non-finite number."); + Add("Round(Ln(2), 2)", 0.69d); + + // Converts letters in a string of text to all lowercase. + Add("Lower(\"HELLO, world\")", "hello, world"); + + // Maximum value of a table expression or a set of arguments. + Add("Max(1, 2, 3)", 3m); + + // Returns the middle portion of a string. + Add("Mid(\"Hello, world!\", 6, 2)", ", "); + + // Minimum value of a table expression or a set of arguments. + Add("Min(1, 2, 3)", 1m); + + // Retrieves the minute portion of a date/time value. + Add("Minute(DateTime(2025, 1, 1, 12, 30, 40))", 30m); + + // Returns the remainder after a dividend is divided by a divisor. + Add("Mod(42, 2)", 0m); + Add("Mod(87, 14)", 3m); + + // Retrieves the month portion of a date/time value. + Add("Month(DateTime(2025, 1, 1, 12, 30, 40))", 1m); + + // Boolean logic NOT. Returns true if its argument is false, and returns false if its argument is true. You can also use the ! operator. + Add("Not(true)", false); + Add("Not(false)", true); + + // Returns the current date/time value in the user's time zone. + Add("Now()", "DateTime.Now"); + + // Boolean logic OR. Returns true if any of its arguments are true. You can also use the || operator. + Add("Or(true, false)", true); + Add("Or(true, true)", true); + Add("Or(false, true)", true); + Add("Or(false, false)", false); + Add("true || false", true); + + // Returns a number raised to a power. You can also use the ^ operator. + Add("Power(10, 2)", 100d); + Add("Power(2, 10)", 1024d); + Add("2 ^ 10", 1024d); + + // Replaces part of a string with another string, by starting position of the string. + Add("Replace(\"Hello, world!\", 1, 5, \"Goodbye\")", "Goodbye, world!"); + + // Returns the right-most portion of a string. + Add("Right(\"Hello, world!\", 6)", "world!"); + + // Exp(): Returns e raised to a power. + // Round(): Rounds to the closest number. + Add("Round(Exp(2), 3)", 7.389d); + Add("Round(Exp(2), 2)", 7.39d); + + // Rounds down to the largest previous number. + Add("RoundDown(10.57443, 2)", 10.57m); + Add("RoundDown(10.57443, 1)", 10.5m); + Add("RoundDown(10.57443, 0)", 10m); + + // Rounds up to the smallest next number. + Add("RoundUp(10.57443, 2)", 10.58m); + Add("RoundUp(10.57443, 1)", 10.6m); + Add("RoundUp(10.57443, 0)", 11m); + + // Retrieves the second portion of a date/time value. + Add("Second(DateTime(2025, 1, 1, 12, 30, 40))", 40m); + + // Returns the square root of a number. + Add("Sqrt(16)", 4d); + + // Checks if a text string begins with another text string. + Add("StartsWith(\"Hello, world!\", \"Hello\")", true); + Add("StartsWith(\"Hello, world!\", \"world!\")", false); + + // Replaces part of a string with another string, by matching strings. + Add("Substitute(\"Hello, world!\", \"Hello\", \"Goodbye\")", "Goodbye, world!"); + + // Calculates the sum of a table expression or a set of arguments. + Add("Sum(1, 2, 3, 4, 5)", 15m); + + // Matches with a set of values and then evaluates a corresponding formula. + Add("Switch(10, 10, \"Result 1\", 20, \"Result 2\", \"Result 3\")", "Result 1"); + Add("Switch(20, 10, \"Result 1\", 20, \"Result 2\", \"Result 3\")", "Result 2"); + Add("Switch(30, 10, \"Result 1\", 20, \"Result 2\", \"Result 3\")", "Result 3"); + + // Removes extra spaces from the ends and interior of a string of text. + Add("Trim(\" Hello, world! \")", "Hello, world!"); + + // Truncates the number to only the integer portion by removing any decimal portion. + Add("Trunc(10.5)", 10m); + + // Removes extra spaces from the ends of a string of text only. + Add("TrimEnds(\" Hello, world! \")", "Hello, world!"); + + // Converts letters in a string of text to all uppercase. + Add("Upper(\"Hello, WORLD!\")", "HELLO, WORLD!"); + + // Converts a string to a number. + Add("Value(10)", 10m); + Add("Value(10.5)", 10.5m); + Add("Value(10.575)", 10.575m); + + // Retrieves the weekday portion of a date/time value. + Add("Weekday(DateTime(2025, 1, 1, 12, 30, 40))", 4m); + + // Returns the week number of a date/time value. + Add("WeekNum(DateTime(2025, 1, 1, 12, 30, 40))", 1m); + Add("WeekNum(DateTime(2025, 1, 1, 12, 30, 40), StartOfWeek.Sunday)", 1m); + Add("WeekNum(DateTime(2025, 1, 1, 12, 30, 40), StartOfWeek.Monday)", 1m); + + // Retrieves the year portion of a date/time value. + Add("Year(DateTime(2025, 1, 1, 12, 30, 40))", 2025m); + } + } + + public class TextFunctionCases : TheoryData + { + public TextFunctionCases() + { + // Converts any value and formats a number or date/time value to a string of text. + Add("Text(1234.59, \"####.#\")", "1234.6"); + Add("Text(8.9, \"#.000\")", "8.900"); + Add("Text(0.631, \"0.#\")", "0.6"); + Add("Text(12, \"#.0#\")", "12.0"); + Add("Text(1234.568, \"#.0#\")", "1234.57"); + Add("Text(12000, \"$ #,###\")", "$ 12,000"); + Add("Text(1200000, \"$ #,###\")", "$ 1,200,000"); + + // TODO: Something funky is going on with the culture handling here. + // TODO: Also, seems LongTime24 just... doesn't? It returns the same as LongTime. + + var testDateTime = new DateTime(2015, 11, 23, 14, 37, 47); + Add("Text(DateTime(2015, 11, 23, 14, 37, 47), DateTimeFormat.LongDate)", testDateTime.ToString("D", CultureInfo.InvariantCulture)); + Add("Text(DateTime(2015, 11, 23, 14, 37, 47), DateTimeFormat.LongDateTime)", testDateTime.ToString("F", CultureInfo.InvariantCulture)); + Add("Text(DateTime(2015, 11, 23, 14, 37, 47), DateTimeFormat.LongTime24)", "14:37:47"); + Add("Text(DateTime(2015, 11, 23, 14, 37, 47), DateTimeFormat.ShortDate)", "11/23/2015"); + Add("Text(DateTime(2015, 11, 23, 14, 37, 47), \"d-mmm-yy\")", "23-Nov-15"); + + // TODO: This errors out with invalid date time + //Add("Text(1448318857 * 1000, \"mmm. dd, yyyy (hh:mm:ss AM/PM)\")", "Nov. 23, 2015 (02:47:37 PM)"); + + // TODO: The in-format specifier doesn't seem to be supported on the parser currently + //Add("Text(1234567, 89; \"[$-da-DK]# ###,## €\")", "1.234.567,89 €"); + Add("Text(1234567.89, \"#,###.## €\", \"da-DK\")", "1.234.567,89 €"); + Add("Text(Date(2016, 1, 31), \"dddd mmmm d\")", "Sunday January 31"); + Add("Text(Date(2016, 1, 31), \"dddd mmmm d\", \"es-ES\")", "domingo enero 31"); + + Add("Text(1234567.89)", "1234567.89"); + + // TODO: This also fails + //Add("Text(DateTimeValue(\"01/04/2003\"))", "1/4/2003 12:00 AM"); + + Add("Text(true)", "true"); + Add("Text(GUID(\"f8b10550-0f12-4f08-9aa3-bb10958bc3ff\"))", "f8b10550-0f12-4f08-9aa3-bb10958bc3ff"); + } } } diff --git a/tests/XrmMockup365Test/UnitTestBase.cs b/tests/XrmMockup365Test/UnitTestBase.cs index 4c056603..3f7aeaab 100644 --- a/tests/XrmMockup365Test/UnitTestBase.cs +++ b/tests/XrmMockup365Test/UnitTestBase.cs @@ -12,37 +12,78 @@ namespace DG.XrmMockupTest { - public class UnitTestBase : IClassFixture + public abstract class ServiceWrapper { - private static DateTime _startTime { get; set; } - #if DATAVERSE_SERVICE_CLIENT - protected IOrganizationServiceAsync2 orgAdminUIService; - protected IOrganizationServiceAsync2 orgAdminService; - protected IOrganizationServiceAsync2 orgGodService; - protected IOrganizationServiceAsync2 orgRealDataService; - - protected IOrganizationServiceAsync2 testUser1Service; - protected IOrganizationServiceAsync2 testUser2Service; - protected IOrganizationServiceAsync2 testUser3Service; - protected IOrganizationServiceAsync2 testUser4Service; + public IOrganizationServiceAsync2 orgAdminUIService { get; protected set; } + public IOrganizationServiceAsync2 orgAdminService { get; protected set; } + public IOrganizationServiceAsync2 orgGodService { get; protected set; } + public IOrganizationServiceAsync2 orgRealDataService { get; protected set; } + + public IOrganizationServiceAsync2 testUser1Service { get; protected set; } + public IOrganizationServiceAsync2 testUser2Service { get; protected set; } + public IOrganizationServiceAsync2 testUser3Service { get; protected set; } + public IOrganizationServiceAsync2 testUser4Service { get; protected set; } #else - protected IOrganizationService orgAdminUIService; - protected IOrganizationService orgAdminService; - protected IOrganizationService orgGodService; - protected IOrganizationService orgRealDataService; - - protected IOrganizationService testUser1Service; - protected IOrganizationService testUser2Service; - protected IOrganizationService testUser3Service; - protected IOrganizationService testUser4Service; + public IOrganizationService orgAdminUIService { get; protected set; } + public IOrganizationService orgAdminService { get; protected set; } + public IOrganizationService orgGodService { get; protected set; } + public IOrganizationService orgRealDataService { get; protected set; } + + public IOrganizationService testUser1Service { get; protected set; } + public IOrganizationService testUser2Service { get; protected set; } + public IOrganizationService testUser3Service { get; protected set; } + public IOrganizationService testUser4Service { get; protected set; } #endif - protected Entity testUser1; - protected Entity testUser2; - protected Entity testUser3; - protected Entity testUser4; - protected Entity testUser5; + public Entity testUser1 { get; protected set; } + public Entity testUser2 { get; protected set; } + public Entity testUser3 { get; protected set; } + public Entity testUser4 { get; protected set; } + public Entity testUser5 { get; protected set; } + } + + public class UnitTestOrganizationServiceFactory : IOrganizationServiceFactory + { + public UnitTestOrganizationServiceFactory(ServiceWrapper services) + { + Services = services; + } + + private ServiceWrapper Services { get; } + + public IOrganizationService CreateOrganizationService(Guid? userId) + { + if (userId == null || userId == Guid.Empty) + { + return Services.orgAdminService; + } + else if (userId == Services.testUser1.Id) + { + return Services.testUser1Service; + } + else if (userId == Services.testUser2.Id) + { + return Services.testUser2Service; + } + else if (userId == Services.testUser3.Id) + { + return Services.testUser3Service; + } + else if (userId == Services.testUser4.Id) + { + return Services.testUser4Service; + } + else + { + throw new ArgumentException($"Unknown userId: {userId}"); + } + } + } + + public class UnitTestBase : ServiceWrapper, IClassFixture + { + private static DateTime _startTime { get; set; } protected Entity contactWriteAccessTeamTemplate; @@ -192,7 +233,19 @@ private void CreateAccessTeamTemplate(string name, int objectTypeCode, params Ac contactWriteAccessTeamTemplate["defaultaccessrightsmask"] = mask; contactWriteAccessTeamTemplate.Id = orgAdminService.Create(contactWriteAccessTeamTemplate); } - public void Dispose() + + protected TEntity Create(TEntity entity) where TEntity : Entity + { + return Create(orgAdminService, entity); + } + + protected static TEntity Create(IOrganizationService service, TEntity entity) where TEntity : Entity + { + var id = service.Create(entity); + return service.Retrieve(entity.LogicalName, id, new Microsoft.Xrm.Sdk.Query.ColumnSet(true)).ToEntity(); + } + + public virtual void Dispose() { // No need to reset environment since each test has its own instance // The instance will be garbage collected automatically