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