From 6542d2eff0f20723660fb7f6c9c5053163637a7c Mon Sep 17 00:00:00 2001 From: Bill Armstrong Date: Mon, 22 Oct 2018 14:35:24 -0400 Subject: [PATCH] Fixed several issues with the creation of the TimeZoneDefinition object: - Some time zones are created with missing TimeZonePeriod objects, resulting in an invalid TimeZoneDefinition structure that is rejected by the EWS service. - The TimeZoneDefinition constructor handles time zones with undefined years BEFORE the collection of AdjustmentRules and AFTER the collection of AdjustmentRules, but it does not handle gaps IN BETWEEN AdjustmentRules. For example, say you have an adjustment rule from 2009 to 2010 and the next adjustment rule is from 2012-1016. In between 2010 and 2012 is the year 2011. This missing year indicates that there was no DST period during 2011. There needs to be a TimeZoneTransition for 2011 to indicate that there is no DST for that year. - Some time zones have years where the standard period BIAS is different than the base BIAS defined in the TimeZoneInfo object. This is indicated by an AdjustmentRule that has a non-zero BaseUtcOffsetDelta property. Currently the EWS library does not take this value into account. By not taking this value into account, the created TimeZoneDefinition will have periods where the defined BIAS for the period is incorrect. NOTE that this is an internal property that can only be accessed through reflection.This issue only affects historical years. The current year is not affected (since the TimeZoneInfo object's bias is based on the current year), and at the time of this bug fix there are no time zones that have this problem for future years. - The value written to the DateTime element on the AbsoluteDateTransition element is converted to UTC. The DateTime is primarily used to specify the start and end year for the transition. Converting the date to UTC may change the start and end year (depending on the time zone) that is written to XML. The date that is written to this element should not be coverted to UTC. - There is one time zone (Sao Tome Standard Time) that has a year with a DST period that lasts one hour. This time zone is rejected as an invalid time zone by EWS. The library was updated to treat years with DST periods lasting less than a day the same as years with no DST at all. This allows the time zone to be received by Exchange with no errors. --- .../TimeZones/AbsoluteDateTransition.cs | 6 +- .../TimeZones/TimeZoneDefinition.cs | 130 ++++-- .../TimeZones/TimeZoneInfoExtensionMethods.cs | 48 +++ .../TimeZones/TimeZoneTransitionGroup.cs | 57 +-- Core/Requests/SetUserPhotoRequest.cs | 2 +- EwsManagedApiTest/EwsManagedApiTest.csproj | 95 +++++ EwsManagedApiTest/ExtensionMethods.cs | 25 ++ EwsManagedApiTest/Properties/AssemblyInfo.cs | 36 ++ EwsManagedApiTest/TimeZoneTest.cs | 402 ++++++++++++++++++ Microsoft.Exchange.WebServices.Data.csproj | 23 +- Microsoft.Exchange.WebServices.Data.sln | 10 +- Properties/AssemblyInfo.cs | 4 +- 12 files changed, 747 insertions(+), 91 deletions(-) create mode 100644 ComplexProperties/TimeZones/TimeZoneInfoExtensionMethods.cs create mode 100644 EwsManagedApiTest/EwsManagedApiTest.csproj create mode 100644 EwsManagedApiTest/ExtensionMethods.cs create mode 100644 EwsManagedApiTest/Properties/AssemblyInfo.cs create mode 100644 EwsManagedApiTest/TimeZoneTest.cs diff --git a/ComplexProperties/TimeZones/AbsoluteDateTransition.cs b/ComplexProperties/TimeZones/AbsoluteDateTransition.cs index 9d7d6c11..38c36b8e 100644 --- a/ComplexProperties/TimeZones/AbsoluteDateTransition.cs +++ b/ComplexProperties/TimeZones/AbsoluteDateTransition.cs @@ -85,10 +85,14 @@ internal override void WriteElementsToXml(EwsServiceXmlWriter writer) { base.WriteElementsToXml(writer); + // Write the DateTime element as a datetime value formatted with no time zone conversions. + // We must not pass the dateTime value to WriteElementValue as a DateTime value, because + // WriteElementValue would convert the DateTime value to UTC using the time zone + // on the ExchangeService object. No time zone conversions should be done on transition objects. writer.WriteElementValue( XmlNamespace.Types, XmlElementNames.DateTime, - this.dateTime); + dateTime.ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture)); } /// diff --git a/ComplexProperties/TimeZones/TimeZoneDefinition.cs b/ComplexProperties/TimeZones/TimeZoneDefinition.cs index cd1491e2..c045a9bb 100644 --- a/ComplexProperties/TimeZones/TimeZoneDefinition.cs +++ b/ComplexProperties/TimeZones/TimeZoneDefinition.cs @@ -93,39 +93,25 @@ internal TimeZoneDefinition(TimeZoneInfo timeZoneInfo) this.Id = timeZoneInfo.Id; this.Name = timeZoneInfo.DisplayName; - // TimeZoneInfo only supports one standard period, which bias is the time zone's base + // TimeZoneInfo only supports one standard period, whose bias is the time zone's base // offset to UTC. - TimeZonePeriod standardPeriod = new TimeZonePeriod(); - standardPeriod.Id = TimeZonePeriod.StandardPeriodId; - standardPeriod.Name = TimeZonePeriod.StandardPeriodName; - standardPeriod.Bias = -timeZoneInfo.BaseUtcOffset; - - TimeZoneInfo.AdjustmentRule[] adjustmentRules = timeZoneInfo.GetAdjustmentRules(); + TimeZonePeriod standardPeriod = this.CreateStandardPeriod(timeZoneInfo); - TimeZoneTransition transitionToStandardPeriod = new TimeZoneTransition(this, standardPeriod); + TimeZoneInfo.AdjustmentRule[] adjustmentRules = timeZoneInfo.GetAdjustmentRules(); if (adjustmentRules.Length == 0) { - this.periods.Add(standardPeriod.Id, standardPeriod); - // If the time zone info doesn't support Daylight Saving Time, we just need to // create one transition to one group with one transition to the standard period. - TimeZoneTransitionGroup transitionGroup = new TimeZoneTransitionGroup(this, "0"); - transitionGroup.Transitions.Add(transitionToStandardPeriod); - - this.transitionGroups.Add(transitionGroup.Id, transitionGroup); - - TimeZoneTransition initialTransition = new TimeZoneTransition(this, transitionGroup); - - this.transitions.Add(initialTransition); + this.transitions.Add(new TimeZoneTransition(this, + CreateTransitionGroupToPeriod(standardPeriod))); } else { for (int i = 0; i < adjustmentRules.Length; i++) { TimeZoneTransitionGroup transitionGroup = new TimeZoneTransitionGroup(this, this.transitionGroups.Count.ToString()); - transitionGroup.InitializeFromAdjustmentRule(adjustmentRules[i], standardPeriod); - + transitionGroup.InitializeFromAdjustmentRule(timeZoneInfo, adjustmentRules[i]); this.transitionGroups.Add(transitionGroup.Id, transitionGroup); TimeZoneTransition transition; @@ -137,17 +123,15 @@ internal TimeZoneDefinition(TimeZoneInfo timeZoneInfo) // period and a group containing the transitions mapping to the adjustment rule. if (adjustmentRules[i].DateStart > DateTime.MinValue.Date) { - TimeZoneTransition transitionToDummyGroup = new TimeZoneTransition( - this, - this.CreateTransitionGroupToPeriod(standardPeriod)); - - this.transitions.Add(transitionToDummyGroup); + // Add the dummy transition for the standard period. + this.transitions.Add(new TimeZoneTransition(this, + this.CreateTransitionGroupToPeriod(standardPeriod))); + // Add the transition corresponding with the adjustment rule's start date. AbsoluteDateTransition absoluteDateTransition = new AbsoluteDateTransition(this, transitionGroup); absoluteDateTransition.DateTime = adjustmentRules[i].DateStart; transition = absoluteDateTransition; - this.periods.Add(standardPeriod.Id, standardPeriod); } else { @@ -156,6 +140,17 @@ internal TimeZoneDefinition(TimeZoneInfo timeZoneInfo) } else { + if ((adjustmentRules[i - 1].DateEnd.Year + 1) != adjustmentRules[i].DateStart.Year) + { + // If the next adjustment rule does not start the year after the previous adjustment rule ends, + // we need to add a dummy transition to cover the years in between. + AbsoluteDateTransition transitionToDummyGroup = new AbsoluteDateTransition(this, + CreateTransitionGroupToPeriod(standardPeriod)); + transitionToDummyGroup.DateTime = adjustmentRules[i - 1].DateEnd.AddDays(1); + + this.transitions.Add(transitionToDummyGroup); + } + AbsoluteDateTransition absoluteDateTransition = new AbsoluteDateTransition(this, transitionGroup); absoluteDateTransition.DateTime = adjustmentRules[i].DateStart; @@ -183,6 +178,89 @@ internal TimeZoneDefinition(TimeZoneInfo timeZoneInfo) } } + /// + /// Creates the standard period for the time zone and adds it to the Period collection. + /// + /// The TimeZoneInfo object whose standard period is to be created. + /// The standard period that was created. + private TimeZonePeriod CreateStandardPeriod(TimeZoneInfo timeZoneInfo) + { + TimeZonePeriod standardPeriod = new TimeZonePeriod(); + standardPeriod.Id = TimeZonePeriod.StandardPeriodId; + standardPeriod.Name = TimeZonePeriod.StandardPeriodName; + standardPeriod.Bias = -timeZoneInfo.BaseUtcOffset; + + this.periods.Add(standardPeriod.Id, standardPeriod); + + return standardPeriod; + } + + /// + /// Searches the period collection for a standard period that matches the specifications of the provided adjustment rule. + /// If one is found, it is returned. + /// If none are found, a new one is created, added to the collection and returned. + /// + /// The TimeZoneInfo object that the adjustmentRule applies to. + /// The AdjustmentRule object that we need a period for. + /// The TimeZonePeriod object that was found or created. + internal TimeZonePeriod CreateStandardPeriodForAdjustmentRule(TimeZoneInfo timeZoneInfo, TimeZoneInfo.AdjustmentRule adjustmentRule) + { + var baseUtcOffsetDelta = adjustmentRule.GetBaseUtcOffsetDelta(); + + if (baseUtcOffsetDelta == TimeSpan.Zero) + { + // The standard bias of this adjustment rule matches the standard bias for the time zone. Use the standard period. + return this.periods[TimeZonePeriod.StandardPeriodId]; + } + else + { + TimeSpan periodBias = -timeZoneInfo.BaseUtcOffset - baseUtcOffsetDelta; + String periodId = String.Format("{0}{1}", TimeZonePeriod.StandardPeriodId, (int)periodBias.TotalMinutes); + + TimeZonePeriod period; + if (!this.periods.TryGetValue(periodId, out period)) + { + period = new TimeZonePeriod(); + period.Id = periodId; + period.Name = TimeZonePeriod.StandardPeriodName; + period.Bias = periodBias; + + this.periods.Add(periodId, period); + } + + return period; + } + } + + /// + /// Searches the period collection for a daylight period that matches the specifications of the provided adjustment rule. + /// If one is found, it is returned. + /// If none are found, a new one is created, added to the collection and returned. + /// + /// The TimeZoneInfo object that the adjustmentRule applies to. + /// The AdjustmentRule object that we need a period for. + /// The TimeZonePeriod object that was found or created. + internal TimeZonePeriod CreateDaylightPeriodForAdjustmentRule(TimeZoneInfo timeZoneInfo, TimeZoneInfo.AdjustmentRule adjustmentRule) + { + var baseUtcOffsetDelta = adjustmentRule.GetBaseUtcOffsetDelta(); + + TimeSpan periodBias = -timeZoneInfo.BaseUtcOffset - baseUtcOffsetDelta - adjustmentRule.DaylightDelta; + String periodId = String.Format("{0}{1}", TimeZonePeriod.DaylightPeriodId, (int)periodBias.TotalMinutes); + + TimeZonePeriod period; + if (!this.periods.TryGetValue(periodId, out period)) + { + period = new TimeZonePeriod(); + period.Id = periodId; + period.Name = TimeZonePeriod.DaylightPeriodName; + period.Bias = periodBias; + + this.periods.Add(periodId, period); + } + + return period; + } + /// /// Adds a transition group with a single transition to the specified period. /// diff --git a/ComplexProperties/TimeZones/TimeZoneInfoExtensionMethods.cs b/ComplexProperties/TimeZones/TimeZoneInfoExtensionMethods.cs new file mode 100644 index 00000000..74b7513f --- /dev/null +++ b/ComplexProperties/TimeZones/TimeZoneInfoExtensionMethods.cs @@ -0,0 +1,48 @@ +using System; +using System.Reflection; + +namespace Microsoft.Exchange.WebServices.Data +{ + /// + /// Utility class for declaring time zone related extension methods. + /// + public static class TimeZoneInfoExtensionMethods + { + /// + /// Extension method to return the internal BaseUtcOffsetDelta property value from an AdjustmentRule. + /// + /// The adjustement rule whose BaseUtcOffsetDelta value should be returned. + /// A TimeSpan value that reprensents the AdjustmentRule's BaseUtcOffsetDelta value. + public static TimeSpan GetBaseUtcOffsetDelta(this TimeZoneInfo.AdjustmentRule adjustmentRule) + { + BindingFlags bindFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + PropertyInfo property = typeof(TimeZoneInfo.AdjustmentRule).GetProperty("BaseUtcOffsetDelta", bindFlags); + return (TimeSpan)property.GetValue(adjustmentRule, null); + } + + /// + /// Extension method to determine if two TransitionTime values reference the same day. + /// + /// The first TransitionTime to compare. + /// The second TransitionTime to compare. + /// True if both TransitionTime objects reference the same day, otherwise false. + public static bool HasSameDate(this TimeZoneInfo.TransitionTime thisTransitionTime, TimeZoneInfo.TransitionTime otherTransitionTime) + { + if (thisTransitionTime.IsFixedDateRule && otherTransitionTime.IsFixedDateRule) + { + return (thisTransitionTime.Month == otherTransitionTime.Month) + && (thisTransitionTime.Day == otherTransitionTime.Day); + } + else if (!thisTransitionTime.IsFixedDateRule && !otherTransitionTime.IsFixedDateRule) + { + return (thisTransitionTime.Month == otherTransitionTime.Month) + && (thisTransitionTime.Week == otherTransitionTime.Week) + && (thisTransitionTime.DayOfWeek == otherTransitionTime.DayOfWeek); + } + else + { + return false; + } + } + } +} diff --git a/ComplexProperties/TimeZones/TimeZoneTransitionGroup.cs b/ComplexProperties/TimeZones/TimeZoneTransitionGroup.cs index 180130d3..d4aa696d 100644 --- a/ComplexProperties/TimeZones/TimeZoneTransitionGroup.cs +++ b/ComplexProperties/TimeZones/TimeZoneTransitionGroup.cs @@ -114,62 +114,39 @@ internal override void WriteElementsToXml(EwsServiceXmlWriter writer) /// /// Initializes this transition group based on the specified asjustment rule. /// + /// The time zone that the adjustment rule applies to. /// The adjustment rule to initialize from. - /// A reference to the pre-created standard period. - internal virtual void InitializeFromAdjustmentRule(TimeZoneInfo.AdjustmentRule adjustmentRule, TimeZonePeriod standardPeriod) + internal virtual void InitializeFromAdjustmentRule(TimeZoneInfo timeZoneInfo, TimeZoneInfo.AdjustmentRule adjustmentRule) { - if (adjustmentRule.DaylightDelta.TotalSeconds == 0) + if ((adjustmentRule.DaylightDelta == TimeSpan.Zero) + || adjustmentRule.DaylightTransitionStart.HasSameDate(adjustmentRule.DaylightTransitionEnd)) { - // If the time zone info doesn't support Daylight Saving Time, we just need to - // create one transition to one group with one transition to the standard period. - TimeZonePeriod standardPeriodToSet = new TimeZonePeriod(); - standardPeriodToSet.Id = string.Format( - "{0}/{1}", - standardPeriod.Id, - adjustmentRule.DateStart.Year); - standardPeriodToSet.Name = standardPeriod.Name; - standardPeriodToSet.Bias = standardPeriod.Bias; - this.timeZoneDefinition.Periods.Add(standardPeriodToSet.Id, standardPeriodToSet); - - this.transitionToStandard = new TimeZoneTransition(this.timeZoneDefinition, standardPeriodToSet); - this.transitions.Add(this.transitionToStandard); + // If the admustment rule does not support daylight savings time, + // the transition must target a transition group that targets a single period. + // If the adjustment rule supports daylight savings time, but the DST start and end are on the same day, + // we treat is as through there is no DST period at all. This is because EWS would reject it as an invalid + // time zone. This is true for the "Sao Tome Standard Time" time zone, which has a DST period that lasts only + // one hour (sounds strange, but there is actually a valid reason for it). + this.transitionToStandard = new TimeZoneTransition(this.timeZoneDefinition, this.timeZoneDefinition.CreateStandardPeriodForAdjustmentRule(timeZoneInfo, adjustmentRule)); } else { - TimeZonePeriod daylightPeriod = new TimeZonePeriod(); - - // Generate an Id of the form "Daylight/2008" - daylightPeriod.Id = string.Format( - "{0}/{1}", - TimeZonePeriod.DaylightPeriodId, - adjustmentRule.DateStart.Year); - daylightPeriod.Name = TimeZonePeriod.DaylightPeriodName; - daylightPeriod.Bias = standardPeriod.Bias - adjustmentRule.DaylightDelta; - - this.timeZoneDefinition.Periods.Add(daylightPeriod.Id, daylightPeriod); - + // If the adjustment rule supports daylight savings time, the transition must target a standard transition group. + // with two transitions; one to DST, and a second back to Standard time. this.transitionToDaylight = TimeZoneTransition.CreateTimeZoneTransition( this.timeZoneDefinition, - daylightPeriod, + timeZoneDefinition.CreateDaylightPeriodForAdjustmentRule(timeZoneInfo, adjustmentRule), adjustmentRule.DaylightTransitionStart); - TimeZonePeriod standardPeriodToSet = new TimeZonePeriod(); - standardPeriodToSet.Id = string.Format( - "{0}/{1}", - standardPeriod.Id, - adjustmentRule.DateStart.Year); - standardPeriodToSet.Name = standardPeriod.Name; - standardPeriodToSet.Bias = standardPeriod.Bias; - this.timeZoneDefinition.Periods.Add(standardPeriodToSet.Id, standardPeriodToSet); - this.transitionToStandard = TimeZoneTransition.CreateTimeZoneTransition( this.timeZoneDefinition, - standardPeriodToSet, + timeZoneDefinition.CreateStandardPeriodForAdjustmentRule(timeZoneInfo, adjustmentRule), adjustmentRule.DaylightTransitionEnd); this.transitions.Add(this.transitionToDaylight); - this.transitions.Add(this.transitionToStandard); } + + this.transitions.Add(this.transitionToStandard); } /// diff --git a/Core/Requests/SetUserPhotoRequest.cs b/Core/Requests/SetUserPhotoRequest.cs index 33dff1b0..0bfad927 100644 --- a/Core/Requests/SetUserPhotoRequest.cs +++ b/Core/Requests/SetUserPhotoRequest.cs @@ -171,7 +171,7 @@ private static SetUserPhotoResponse SetResultOrDefault(Func serviceRespo { return (SetUserPhotoResponse)serviceResponseFactory(); } - catch (ServiceRequestException ex) + catch (ServiceRequestException) { throw; } diff --git a/EwsManagedApiTest/EwsManagedApiTest.csproj b/EwsManagedApiTest/EwsManagedApiTest.csproj new file mode 100644 index 00000000..e91f8c05 --- /dev/null +++ b/EwsManagedApiTest/EwsManagedApiTest.csproj @@ -0,0 +1,95 @@ + + + + Debug + AnyCPU + {68BFE270-3677-422A-9394-C0D8FECDC28B} + Library + Properties + EwsManagedApiTest + EwsManagedApiTest + v3.5 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + SAK + SAK + SAK + SAK + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + {f059972f-0561-4203-abb8-3abb41ccbe22} + Microsoft.Exchange.WebServices.Data + + + + + + + False + + + False + + + False + + + False + + + + + + + + \ No newline at end of file diff --git a/EwsManagedApiTest/ExtensionMethods.cs b/EwsManagedApiTest/ExtensionMethods.cs new file mode 100644 index 00000000..2eb14548 --- /dev/null +++ b/EwsManagedApiTest/ExtensionMethods.cs @@ -0,0 +1,25 @@ +using Microsoft.Exchange.WebServices.Data; +using System.Collections.Generic; +using System.Reflection; + +namespace EwsManagedApiTest +{ + /// + /// A class for extension methods to assist. + /// + static class ExtensionMethods + { + /// + /// Returns the collection of TimeZoneTransitions that are contained within the TimeZoneDefinition object. + /// Uses reflection to access the private field member. + /// + /// The TimeZoneDefinition object whose transitions are to be returned. + /// The collection of TimeZoneTransition objects. + public static List GetTransitions(this TimeZoneDefinition timeZoneDefinition) + { + BindingFlags bindFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + FieldInfo field = typeof(TimeZoneDefinition).GetField("transitions", bindFlags); + return field.GetValue(timeZoneDefinition) as List; + } + } +} diff --git a/EwsManagedApiTest/Properties/AssemblyInfo.cs b/EwsManagedApiTest/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..e3d12402 --- /dev/null +++ b/EwsManagedApiTest/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("EwsManagedApiTest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("EwsManagedApiTest")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("68bfe270-3677-422a-9394-c0d8fecdc28b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/EwsManagedApiTest/TimeZoneTest.cs b/EwsManagedApiTest/TimeZoneTest.cs new file mode 100644 index 00000000..c5e657d5 --- /dev/null +++ b/EwsManagedApiTest/TimeZoneTest.cs @@ -0,0 +1,402 @@ +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Exchange.WebServices.Data; +using System.Collections.Generic; +using System.Diagnostics; + +namespace EwsManagedApiTest +{ + /// + /// A collection unit tests for testing EWS's Time Zone classes. + /// + [TestClass] + public class TimeZoneTest + { + /// + /// An enumeration used to classify a time zone period as either a standard period or a daylight period. + /// + public enum TimeZonePeriodType + { + Standard, + Daylight + } + + /// + /// Verifies that the TimeZoneDefinition contains the period identified by the provided ID string, and that the period + /// matches the values in the specified TimeZoneInfo object and optionally a specific AdjustmentRule. + /// + /// The TimeZoneDefinion object containing the period to validate. + /// The ID of the period to validate. + /// The TimeZoneInfo object used to construct the TimeZoneDefinition object. + /// An optional adjustment rule that applies to the period being validated. + /// A enum value indicating whether the period was a Daylight period or a Standard period. + private TimeZonePeriodType ValidateTimeZonePeriod(TimeZoneDefinition tzd, String periodId, TimeZoneInfo tzInfo, TimeZoneInfo.AdjustmentRule adjustmentRule) + { + TimeZonePeriod period; + + Assert.IsTrue(tzd.Periods.TryGetValue(periodId, out period), "The period was not found in the Time Zone Definition."); + + TimeSpan expectedBias; + TimeZonePeriodType periodType; + + if (period.IsStandardPeriod) + { + expectedBias = tzInfo.BaseUtcOffset; + if (adjustmentRule != null) + { + expectedBias += adjustmentRule.GetBaseUtcOffsetDelta(); + } + + periodType = TimeZonePeriodType.Standard; + } + else + { + Assert.IsNotNull(adjustmentRule, "A daylight period was encountered without a matching adjustment rule."); + + expectedBias = tzInfo.BaseUtcOffset + adjustmentRule.GetBaseUtcOffsetDelta() + adjustmentRule.DaylightDelta; + + periodType = TimeZonePeriodType.Daylight; + } + + Assert.AreEqual(expectedBias, TimeSpan.Zero - period.Bias, "A period bias was not correct."); + return periodType; + } + + /// + /// Verifies that the TimeZoneDefinition contains the transition group identified by the provided ID string, + /// and that the transition group represents a year with no DST period and that the group + /// matches the values in the specified TimeZoneInfo object and optionally a specific AdjustmentRule. + /// + /// The TimeZoneDefinion object containing the transition group to validate. + /// The ID of the transition group to validate. + /// The TimeZoneInfo object used to construct the TimeZoneDefinition object. + /// An optional adjustment rule that applies to the transition group being validated. + private void ValidateStandardTransitionGroup(TimeZoneDefinition tzd, string groupId, TimeZoneInfo tzInfo, TimeZoneInfo.AdjustmentRule adjustmentRule) + { + TimeZoneTransitionGroup transitionGroup; + + Assert.IsTrue(tzd.TransitionGroups.TryGetValue(groupId, out transitionGroup), "The transition group was not found in the time zone definition."); + if (adjustmentRule != null) + { + if (adjustmentRule.DaylightDelta != TimeSpan.Zero) + { + Assert.IsTrue(adjustmentRule.DaylightTransitionStart.HasSameDate(adjustmentRule.DaylightTransitionEnd), "Period transition groups must not be associated with an adjustment rule that has DST enabled unless the DST period starts and ends on the same day."); + } + } + Assert.AreEqual(1, transitionGroup.Transitions.Count, "A period transition group should have only one transition."); + + TimeZoneTransition transition = transitionGroup.Transitions[0]; + + Assert.IsNotNull(transition.TargetPeriod, "A transition within a transition group must contain a period."); + + Assert.AreEqual(TimeZonePeriodType.Standard, ValidateTimeZonePeriod(tzd, transition.TargetPeriod.Id, tzInfo, adjustmentRule), "The periods within a period transition must be a standard period."); + + Assert.IsInstanceOfType(transition, typeof(TimeZoneTransition), "The transition within a period transition group must be a standard transition."); + } + + /// + /// Helper function to verify that two TimeZoneTransition structures represent different days of the year. + /// + /// The first transition date to verify. + /// The second transition date to verify. + private void VerifyTransitionsAreOnDifferentDays(TimeZoneTransition transition1, TimeZoneTransition transition2) + { + if (transition1.GetType() != transition2.GetType()) + { + // Assume that if the transitions are different types, they are on different days. + return; + } + + if (transition1 is AbsoluteDayOfMonthTransition) + { + var absTransition1 = transition1 as AbsoluteDayOfMonthTransition; + var absTransition2 = transition2 as AbsoluteDayOfMonthTransition; + + Assert.IsFalse((absTransition1.Month == absTransition2.Month) && (absTransition1.DayOfMonth == absTransition2.DayOfMonth), "Daylight start and end date must not be the same."); + } + else if (transition1 is RelativeDayOfMonthTransition) + { + var relTransition1 = transition1 as RelativeDayOfMonthTransition; + var relTransition2 = transition2 as RelativeDayOfMonthTransition; + + Assert.IsFalse((relTransition1.Month == relTransition2.Month) + && (relTransition1.WeekIndex == relTransition2.WeekIndex) + && (relTransition1.DayOfTheWeek == relTransition2.DayOfTheWeek), + "Daylight start and end date must not be the same."); + } + else if (transition1 is AbsoluteDateTransition) + { + var absTransition1 = transition1 as AbsoluteDateTransition; + var absTransition2 = transition2 as AbsoluteDateTransition; + + Assert.IsFalse((absTransition1.DateTime.Year == absTransition2.DateTime.Year) + && (absTransition1.DateTime.Month == absTransition2.DateTime.Month) + && (absTransition1.DateTime.Day == absTransition2.DateTime.Day), + "Daylight start and end date must not be the same."); + } + } + + /// + /// Verifies that the TimeZoneDefinition contains the transition group identified by the provided ID string, + /// and that the transition group represents a year with a DST period and that the group + /// matches the values in the specified TimeZoneInfo object and optionally a specific AdjustmentRule. + /// + /// The TimeZoneDefinion object containing the transition group to validate. + /// The ID of the transition group to validate. + /// The TimeZoneInfo object used to construct the TimeZoneDefinition object. + /// The adjustment rule that applies to the transition group being validated. + private void ValidateDaylightTransitionGroup(TimeZoneDefinition tzd, string groupId, TimeZoneInfo tzInfo, TimeZoneInfo.AdjustmentRule adjustmentRule) + { + TimeZoneTransitionGroup transitionGroup; + + Assert.IsTrue(tzd.TransitionGroups.TryGetValue(groupId, out transitionGroup), "The transition group was not found in the time zone definition."); + Assert.IsNotNull(adjustmentRule, "Transition groups must be associated with an adjustment rule."); + Assert.AreNotEqual(TimeSpan.Zero, adjustmentRule.DaylightDelta, "Transition groups must be associated with an adjustment rule that has DST enabled."); + Assert.AreEqual(2, transitionGroup.Transitions.Count, "A transition group should have two transitions."); + + VerifyTransitionsAreOnDifferentDays(transitionGroup.Transitions[0], transitionGroup.Transitions[1]); + + TimeZonePeriodType[] timeZonePeriodTypes = new TimeZonePeriodType[2]; + + for (int ix=0; ix<2; ix++) + { + TimeZoneTransition transition = transitionGroup.Transitions[ix]; + + Assert.IsNotNull(transition.TargetPeriod, "A transition within a transition group must contain a period."); + + timeZonePeriodTypes[ix] = ValidateTimeZonePeriod(tzd, transition.TargetPeriod.Id, tzInfo, adjustmentRule); + + if (transition is AbsoluteDayOfMonthTransition) + { + var absTransition = transition as AbsoluteDayOfMonthTransition; + if (timeZonePeriodTypes[ix] == TimeZonePeriodType.Standard) + { + Assert.IsTrue(adjustmentRule.DaylightTransitionEnd.IsFixedDateRule, "Standard transition should not be a fixed date transition."); + Assert.AreEqual((int)adjustmentRule.DaylightTransitionEnd.DayOfWeek, 0, "Standard transition has the wrong DayOfTheWeek."); + Assert.AreEqual(adjustmentRule.DaylightTransitionEnd.Month, absTransition.Month, "Standard transition has the wrong DayOfTheWeek."); + Assert.AreEqual(adjustmentRule.DaylightTransitionEnd.Day, absTransition.DayOfMonth, "Standard transition has the wrong DayOfMonth."); + Assert.AreEqual(adjustmentRule.DaylightTransitionEnd.TimeOfDay.TimeOfDay, absTransition.TimeOffset, "Standard transition has the wrong time offset."); + } + else + { + Assert.IsTrue(adjustmentRule.DaylightTransitionStart.IsFixedDateRule, "Daylight transition should not be a fixed date transition."); + Assert.AreEqual((int)adjustmentRule.DaylightTransitionStart.DayOfWeek, 0, "Daylight transition has the wrong DayOfTheWeek."); + Assert.AreEqual(adjustmentRule.DaylightTransitionStart.Month, absTransition.Month, "Daylight transition has the wrong DayOfTheWeek."); + Assert.AreEqual(adjustmentRule.DaylightTransitionStart.Day, absTransition.DayOfMonth, "Daylight transition has the wrong WeekIndex."); + Assert.AreEqual(adjustmentRule.DaylightTransitionStart.TimeOfDay.TimeOfDay, absTransition.TimeOffset, "Daylight transition has the wrong time offset."); + } + } + else if (transition is RelativeDayOfMonthTransition) + { + var relTransition = transition as RelativeDayOfMonthTransition; + if (timeZonePeriodTypes[ix] == TimeZonePeriodType.Standard) + { + Assert.IsFalse(adjustmentRule.DaylightTransitionEnd.IsFixedDateRule, "Standard transition should be a fixed date transition."); + Assert.AreEqual((int)adjustmentRule.DaylightTransitionEnd.DayOfWeek, (int)relTransition.DayOfTheWeek, "Standard transition has the wrong DayOfTheWeek."); + Assert.AreEqual(adjustmentRule.DaylightTransitionEnd.Month, relTransition.Month, "Standard transition has the wrong DayOfTheWeek."); + Assert.AreEqual((adjustmentRule.DaylightTransitionEnd.Week == 5) ? -1 : adjustmentRule.DaylightTransitionEnd.Week, relTransition.WeekIndex, "Standard transition has the wrong WeekIndex."); + Assert.AreEqual(adjustmentRule.DaylightTransitionEnd.TimeOfDay.TimeOfDay, relTransition.TimeOffset, "Standard transition has the wrong time offset."); + } + else + { + Assert.IsFalse(adjustmentRule.DaylightTransitionStart.IsFixedDateRule, "Daylight transition should be a fixed date transition."); + Assert.AreEqual((int)adjustmentRule.DaylightTransitionStart.DayOfWeek, (int)relTransition.DayOfTheWeek, "Daylight transition has the wrong DayOfTheWeek."); + Assert.AreEqual(adjustmentRule.DaylightTransitionStart.Month, relTransition.Month, "Daylight transition has the wrong DayOfTheWeek."); + Assert.AreEqual((adjustmentRule.DaylightTransitionStart.Week == 5) ? -1 : adjustmentRule.DaylightTransitionStart.Week, relTransition.WeekIndex, "Daylight transition has the wrong WeekIndex."); + Assert.AreEqual(adjustmentRule.DaylightTransitionStart.TimeOfDay.TimeOfDay, relTransition.TimeOffset, "Daylight transition has the wrong time offset."); + } + } + else if (transition is AbsoluteDateTransition) + { + var absTransition = transition as AbsoluteDateTransition; + if (timeZonePeriodTypes[ix] == TimeZonePeriodType.Standard) + { + Assert.IsTrue(adjustmentRule.DaylightTransitionEnd.IsFixedDateRule, "Standard transition should not be a fixed date transition."); + Assert.AreEqual((int)adjustmentRule.DaylightTransitionEnd.DayOfWeek, 0, "Standard transition has the wrong DayOfTheWeek."); + Assert.AreEqual(adjustmentRule.DaylightTransitionEnd.Month, absTransition.DateTime.Month, "Standard transition has the wrong DayOfTheWeek."); + Assert.AreEqual(adjustmentRule.DaylightTransitionEnd.Day, absTransition.DateTime.Day, "Standard transition has the wrong DayOfMonth."); + Assert.IsTrue(absTransition.DateTime.Year > 0, "Standard transition year is not set."); + Assert.AreEqual(adjustmentRule.DaylightTransitionEnd.TimeOfDay.TimeOfDay, absTransition.DateTime.TimeOfDay, "Standard transition has the wrong time offset."); + } + else + { + Assert.IsTrue(adjustmentRule.DaylightTransitionStart.IsFixedDateRule, "Daylight transition should not be a fixed date transition."); + Assert.AreEqual((int)adjustmentRule.DaylightTransitionStart.DayOfWeek, 0, "Daylight transition has the wrong DayOfTheWeek."); + Assert.AreEqual(adjustmentRule.DaylightTransitionStart.Month, absTransition.DateTime.Month, "Daylight transition has the wrong DayOfTheWeek."); + Assert.AreEqual(adjustmentRule.DaylightTransitionStart.Day, absTransition.DateTime.Day, "Daylight transition has the wrong WeekIndex."); + Assert.IsTrue(absTransition.DateTime.Year > 0, "Daylight transition year is not set."); + Assert.AreEqual(adjustmentRule.DaylightTransitionStart.TimeOfDay.TimeOfDay, absTransition.DateTime.TimeOfDay, "Daylight transition has the wrong time offset."); + } + } + else + { + Assert.Fail("Standard transitions are not allowed in a transition group."); + } + } + + Assert.IsTrue(timeZonePeriodTypes[0] != timeZonePeriodTypes[1], "A time zone transition group must contain one standard period and one daylight period."); + } + + /// + /// A helper function that will create a TimeZoneDefinition object from the provided TimeZoneInfo object and + /// verify that the Dynamic DST rules represented by the created TimeZoneDefinition object matches those + /// of the TimeZoneInfo object. + /// + /// The TimeZoneInfo object representing the time zone to be tested. + public void TestTimeZone(TimeZoneInfo timeZoneInfo) + { + TimeZoneDefinition tzd = new TimeZoneDefinition(timeZoneInfo); + var transitions = tzd.GetTransitions(); + if ((transitions == null) || (transitions.Count < 1)) + { + Assert.Fail("No transitions found."); + } + + var adjustmentRules = timeZoneInfo.GetAdjustmentRules(); + if (adjustmentRules.Length < 1) + { + adjustmentRules = null; + } + int adjustmentRuleIndex = -1; + TimeZoneInfo.AdjustmentRule lastAdjustmentRule = null; + + foreach (var transition in transitions) + { + Assert.IsNull(transition.TargetPeriod, "Time zone transitions cannot reference time zone periods."); + Assert.IsNotNull(transition.TargetGroup, "Time zone transitions must referenca a time zone group."); + + TimeZoneInfo.AdjustmentRule adjustmentRule = null; + if (transition.GetType() == typeof(TimeZoneTransition)) + { + if (adjustmentRules != null) + { + // If there are adjustment rules, the first rule will apply to the transition if the date on the rule is MinDate.Date + if (adjustmentRules[0].DateStart == DateTime.MinValue.Date) + { + adjustmentRuleIndex = 0; + adjustmentRule = adjustmentRules[adjustmentRuleIndex]; + } + } + } + else if (transition.GetType() == typeof(AbsoluteDateTransition)) + { + AbsoluteDateTransition absTransition = (AbsoluteDateTransition)transition; + + Assert.IsNotNull(adjustmentRules, "The time zone definition cannot have any absolute date transitions if the time zone does not have any adjustment rules."); + + if (lastAdjustmentRule != null) + { + Assert.AreEqual(lastAdjustmentRule.DateEnd.AddDays(1), absTransition.DateTime, "The transition date should be the next day after the end of the previous adjustment rule."); + + if (adjustmentRuleIndex < (adjustmentRules.Length - 1)) + { + if (absTransition.DateTime < adjustmentRules[adjustmentRuleIndex + 1].DateStart) + { + // We have a hole in between the adjustment rules. + adjustmentRule = null; + } + else if (absTransition.DateTime == adjustmentRules[adjustmentRuleIndex + 1].DateStart) + { + adjustmentRule = adjustmentRules[++adjustmentRuleIndex]; + } + else + { + // Should not be possible, but just in case. + Assert.Fail("The transition start date cannot be greater than the next available adjustment rule."); + } + } + else + { + adjustmentRule = null; + } + } + else + { + if (adjustmentRuleIndex < (adjustmentRules.Length - 1)) + { + if (absTransition.DateTime < adjustmentRules[adjustmentRuleIndex + 1].DateStart) + { + Assert.Fail("A transition found with a start date that is less than the next available adjustment rule."); + } + else if (absTransition.DateTime == adjustmentRules[adjustmentRuleIndex + 1].DateStart) + { + adjustmentRule = adjustmentRules[++adjustmentRuleIndex]; + } + else + { + // Should not be possible, but just in case. + Assert.Fail("The transition start date cannot be greater than the next available adjustment rule."); + } + } + else + { + Assert.Fail("A transition found with no more adjustment rules to pick from."); + } + } + } + else + { + Assert.Fail("Unexpected transition type in the transition collection."); + } + + if (transition.TargetGroup.SupportsDaylight) + { + ValidateDaylightTransitionGroup(tzd, transition.TargetGroup.Id, timeZoneInfo, adjustmentRule); + } + else + { + ValidateStandardTransitionGroup(tzd, transition.TargetGroup.Id, timeZoneInfo, adjustmentRule); + } + + lastAdjustmentRule = adjustmentRule; + } + + if (adjustmentRules != null) + { + Assert.AreEqual(adjustmentRules.Length - 1, adjustmentRuleIndex, "There are unprocessed adjustment rules in the time zone."); + } + + if (lastAdjustmentRule != null) + { + // If the last transition corresponds with an adjustment rule, the adjustment rule should terminate with the Max date value. + Assert.AreEqual(DateTime.MaxValue.Date, lastAdjustmentRule.DateEnd, "The last adjustment rule does not end with the max date value. An additional transition should have been created."); + } + } + + /// + /// A unit test that tests the creation of a TimeZoneDefinion object for every time zone known to the .Net runtime. + /// + [TestMethod] + public void TimeZoneTest_TimeZoneDefinitions() + { + int failureCount = 0; + foreach (TimeZoneInfo timeZoneInfo in TimeZoneInfo.GetSystemTimeZones()) + { + try + { + TestTimeZone(timeZoneInfo); + } + catch(Exception err) + { + //Assert.Fail("The time zone '{0}' failed with error [{1}].", timeZoneInfo.Id, err.Message); + Debug.Print("The time zone '{0}' failed with error [{1}].", timeZoneInfo.Id, err.Message); + failureCount++; + } + } + + Assert.AreEqual(0, failureCount, "One or more time zones failed the TimeZoneDefinition test."); + } + + /// + /// This test is useful for when you need to step through the creation of a specific time zone definition. + /// It is here for debugging purposes only, which is why it has the [Ignore] attribute. + /// + [Ignore] + [TestMethod] + public void TimeZoneTest_TimeZoneDefinition() + { + TestTimeZone(TimeZoneInfo.GetSystemTimeZones().Single(x => x.Id == "Venezuela Standard Time")); + } + } +} diff --git a/Microsoft.Exchange.WebServices.Data.csproj b/Microsoft.Exchange.WebServices.Data.csproj index 39f1a732..970e020e 100644 --- a/Microsoft.Exchange.WebServices.Data.csproj +++ b/Microsoft.Exchange.WebServices.Data.csproj @@ -96,6 +96,7 @@ + @@ -879,41 +880,23 @@ - - - - - - - - - - - + ]]> - - - - - - - - \ No newline at end of file diff --git a/Microsoft.Exchange.WebServices.Data.sln b/Microsoft.Exchange.WebServices.Data.sln index 42b6a0eb..a3725b4f 100644 --- a/Microsoft.Exchange.WebServices.Data.sln +++ b/Microsoft.Exchange.WebServices.Data.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.30324.0 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8BBBEC00-BD81-4F37-A855-D338DC01E1C5}" ProjectSection(SolutionItems) = preProject @@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Exchange.WebServices.Data", "Microsoft.Exchange.WebServices.Data.csproj", "{F059972F-0561-4203-ABB8-3ABB41CCBE22}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EwsManagedApiTest", "EwsManagedApiTest\EwsManagedApiTest.csproj", "{68BFE270-3677-422A-9394-C0D8FECDC28B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -22,6 +24,10 @@ Global {F059972F-0561-4203-ABB8-3ABB41CCBE22}.Debug|Any CPU.Build.0 = Debug|Any CPU {F059972F-0561-4203-ABB8-3ABB41CCBE22}.Release|Any CPU.ActiveCfg = Release|Any CPU {F059972F-0561-4203-ABB8-3ABB41CCBE22}.Release|Any CPU.Build.0 = Release|Any CPU + {68BFE270-3677-422A-9394-C0D8FECDC28B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68BFE270-3677-422A-9394-C0D8FECDC28B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68BFE270-3677-422A-9394-C0D8FECDC28B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68BFE270-3677-422A-9394-C0D8FECDC28B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 8637cbd9..4b664b16 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -24,6 +24,7 @@ */ using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // CLS Compliant @@ -33,4 +34,5 @@ [assembly: ComVisible(false)] -// Friend Assemblies: Add to AssemblyInfoMicrosoft.cs \ No newline at end of file +// Friend Assemblies: Add to AssemblyInfoMicrosoft.cs +[assembly: InternalsVisibleTo("EwsManagedApiTest")] \ No newline at end of file