From 5851fe2c0870a830813ab7c437527db42cb22904 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Sun, 17 Jul 2016 19:27:58 -0400 Subject: [PATCH] REBASE - work on attendee serialization and deserialization. HashCodes and Equality working as expected #45 --- ical.NET.UnitTests/AttendeeTest.cs | 9 +- ical.NET.UnitTests/EqualityAndHashingTests.cs | 74 ++++-- ical.NET.UnitTests/SerializationTests.cs | 5 +- .../SymmetricSerializationTests.cs | 75 ++++++ ical.NET/Components/Event.cs | 3 +- ical.NET/DataTypes/Attachment.cs | 6 +- ical.NET/DataTypes/Attendee.cs | 232 ++++++++++++++---- 7 files changed, 326 insertions(+), 78 deletions(-) diff --git a/ical.NET.UnitTests/AttendeeTest.cs b/ical.NET.UnitTests/AttendeeTest.cs index 67f78475e..fd0aed65a 100644 --- a/ical.NET.UnitTests/AttendeeTest.cs +++ b/ical.NET.UnitTests/AttendeeTest.cs @@ -21,20 +21,19 @@ internal static Event VEventFactory() }; } - private const string _requiredParticipant = "REQ-PARTICIPANT"; //this string may be added to the api in the future private static readonly IList _attendees = new List { new Attendee("MAILTO:james@example.com") { CommonName = "James James", - Role = _requiredParticipant, + Role = ParticipationRole.RequiredParticipant, Rsvp = true, ParticipationStatus = EventParticipationStatus.Tentative }, new Attendee("MAILTO:mary@example.com") { CommonName = "Mary Mary", - Role = _requiredParticipant, + Role = ParticipationRole.RequiredParticipant, Rsvp = true, ParticipationStatus = EventParticipationStatus.Accepted } @@ -54,7 +53,7 @@ public void Add1Attendee() Assert.AreEqual(1, evt.Attendees.Count); //the properties below had been set to null during the Attendees.Add operation in NuGet version 2.1.4 - Assert.AreEqual(_requiredParticipant, evt.Attendees[0].Role); + Assert.AreEqual(ParticipationRole.RequiredParticipant, evt.Attendees[0].Role); Assert.AreEqual(EventParticipationStatus.Tentative, evt.Attendees[0].ParticipationStatus); } @@ -67,7 +66,7 @@ public void Add2Attendees() evt.Attendees.Add(_attendees[0]); evt.Attendees.Add(_attendees[1]); Assert.AreEqual(2, evt.Attendees.Count); - Assert.AreEqual(_requiredParticipant, evt.Attendees[1].Role); + Assert.AreEqual(ParticipationRole.RequiredParticipant, evt.Attendees[1].Role); var cal = new Calendar(); cal.Events.Add(evt); diff --git a/ical.NET.UnitTests/EqualityAndHashingTests.cs b/ical.NET.UnitTests/EqualityAndHashingTests.cs index e715f77d0..c26d9c937 100644 --- a/ical.NET.UnitTests/EqualityAndHashingTests.cs +++ b/ical.NET.UnitTests/EqualityAndHashingTests.cs @@ -139,26 +139,58 @@ public static IEnumerable VTimeZone_TestCases() yield return new TestCaseData(first, second); } - //ToDo: Tests for: - //private IUniqueComponentList _mUniqueComponents; - //private IUniqueComponentList _mEvents; - //private IUniqueComponentList _mTodos; - //private ICalendarObjectList _mJournals; - //private IUniqueComponentList _mFreeBusy; - //private ICalendarObjectList _mTimeZones; - - // FreeBusy - //VAlarm - //Journal - //RecurringComponent? - //Todo - //VTimeZone? - //IAttachment - //GeographicLocation - //Organizer - //StatusCode - //Trigger - //UTCOffset -- perhaps try to retire this entirely first - //WeekDay + [Test, TestCaseSource(nameof(Attendees_TestCases))] + public void Attendees_Tests(Attendee actual, Attendee expected) + { + Assert.AreEqual(expected.GetHashCode(), actual.GetHashCode()); + Assert.AreEqual(expected, actual); + } + + public static IEnumerable Attendees_TestCases() + { + var tentative1 = new Attendee("MAILTO:james@example.com") + { + CommonName = "James Tentative", + Role = ParticipationRole.RequiredParticipant, + Rsvp = true, + ParticipationStatus = EventParticipationStatus.Tentative + }; + var tentative2 = new Attendee("MAILTO:james@example.com") + { + CommonName = "James Tentative", + Role = ParticipationRole.RequiredParticipant, + Rsvp = true, + ParticipationStatus = EventParticipationStatus.Tentative + }; + yield return new TestCaseData(tentative1, tentative2).SetName("Simple attendee test case"); + + var complex1 = new Attendee("MAILTO:mary@example.com") + { + CommonName = "Mary Accepted", + Rsvp = true, + ParticipationStatus = EventParticipationStatus.Accepted, + SentBy = new Uri("mailto:someone@example.com"), + DirectoryEntry = new Uri("ldap://example.com:6666/o=eDABC Industries,c=3DUS??(cn=3DBMary Accepted)"), + Type = "CuType", + Members = new List { "Group A", "Group B"}, + Role = ParticipationRole.Chair, + DelegatedTo = new List { "Peon A", "Peon B"}, + DelegatedFrom = new List { "Bigwig A", "Bigwig B"} + }; + var complex2 = new Attendee("MAILTO:mary@example.com") + { + CommonName = "Mary Accepted", + Rsvp = true, + ParticipationStatus = EventParticipationStatus.Accepted, + SentBy = new Uri("mailto:someone@example.com"), + DirectoryEntry = new Uri("ldap://example.com:6666/o=eDABC Industries,c=3DUS??(cn=3DBMary Accepted)"), + Type = "CuType", + Members = new List { "Group A", "Group B" }, + Role = ParticipationRole.Chair, + DelegatedTo = new List { "Peon A", "Peon B" }, + DelegatedFrom = new List { "Bigwig A", "Bigwig B" } + }; + yield return new TestCaseData(complex1, complex2).SetName("Complex attendee test"); + } } } diff --git a/ical.NET.UnitTests/SerializationTests.cs b/ical.NET.UnitTests/SerializationTests.cs index 62970a102..eb2d333ce 100644 --- a/ical.NET.UnitTests/SerializationTests.cs +++ b/ical.NET.UnitTests/SerializationTests.cs @@ -286,20 +286,19 @@ public void EventPropertiesSerialized() }); } - private const string _requiredParticipant = "REQ-PARTICIPANT"; //this string may be added to the api in the future private static readonly IList _attendees = new List { new Attendee("MAILTO:james@example.com") { CommonName = "James James", - Role = _requiredParticipant, + Role = ParticipationRole.RequiredParticipant, Rsvp = true, ParticipationStatus = EventParticipationStatus.Tentative }, new Attendee("MAILTO:mary@example.com") { CommonName = "Mary Mary", - Role = _requiredParticipant, + Role = ParticipationRole.RequiredParticipant, Rsvp = true, ParticipationStatus = EventParticipationStatus.Accepted } diff --git a/ical.NET.UnitTests/SymmetricSerializationTests.cs b/ical.NET.UnitTests/SymmetricSerializationTests.cs index 0fc48e13c..38efa3b45 100644 --- a/ical.NET.UnitTests/SymmetricSerializationTests.cs +++ b/ical.NET.UnitTests/SymmetricSerializationTests.cs @@ -2,17 +2,28 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using Ical.Net; using Ical.Net.DataTypes; +using Ical.Net.Interfaces; using Ical.Net.Interfaces.DataTypes; using Ical.Net.Serialization; using Ical.Net.Serialization.iCalendar.Serializers; +using Ical.Net.Serialization.iCalendar.Serializers.DataTypes; using NUnit.Framework; +using NUnit.Framework.Interfaces; namespace ical.Net.UnitTests { public class SymmetricSerializationTests { + private static readonly DateTime _nowTime = DateTime.Now; + private static readonly DateTime _later = _nowTime.AddHours(1); + private static CalendarSerializer GetNewSerializer() => new CalendarSerializer(new SerializationContext()); + private static string SerializeToString(Calendar c) => GetNewSerializer().SerializeToString(c); + private static Event GetSimpleEvent() => new Event {DtStart = new CalDateTime(_nowTime), DtEnd = new CalDateTime(_later),Duration = _later - _nowTime}; + private static ICalendar UnserializeCalendar(string s) => Calendar.LoadFromStream(new StringReader(s)).Single(); + [Test] public void SimpleEvent_Test() { @@ -66,5 +77,69 @@ public void VTimeZoneSerialization_Test() Assert.AreEqual(originalCalendar, unserializedCalendar); Assert.AreEqual(originalCalendar.GetHashCode(), unserializedCalendar.GetHashCode()); } + + [Test, TestCaseSource(nameof(AttendeeSerialization_TestCases))] + public void AttendeeSerialization_Test(Attendee attendee) + { + var calendar = new Calendar(); + calendar.AddTimeZone(new VTimeZone("America/Los_Angeles")); + var someEvent = GetSimpleEvent(); + someEvent.Attendees = new List {attendee}; + calendar.Events.Add(someEvent); + + var serialized = SerializeToString(calendar); + var unserialized = UnserializeCalendar(serialized); + + Assert.AreEqual(calendar.GetHashCode(), unserialized.GetHashCode()); + Assert.IsTrue(calendar.Events.SequenceEqual(unserialized.Events)); + Assert.AreEqual(calendar, unserialized); + } + + public static IEnumerable AttendeeSerialization_TestCases() + { + //TODO: Fix this. It appears to be non-deterministic. E.g. you can have one of the Delegated* properties uncommented, and it works, but not both + var complex1 = new Attendee("MAILTO:mary@example.com") + { + CommonName = "Mary Accepted", + Rsvp = true, + ParticipationStatus = EventParticipationStatus.Accepted, + //SentBy = new Uri("mailto:someone@example.com"), //Broken + //DirectoryEntry = new Uri("ldap://example.com:6666/o=eDABC Industries,c=3DUS??(cn=3DBMary Accepted)"), //Broken + //Type = "CuType", + //Members = new List { "Group A", "Group B" }, + //Role = ParticipationRole.Chair, + //DelegatedTo = new List { "Peon A", "Peon B" }, + //DelegatedFrom = new List { "Bigwig A", "Bigwig B" } + }; + yield return new TestCaseData(complex1).SetName("Complex attendee"); + + var simple = new Attendee("MAILTO:james@example.com") + { + CommonName = "James James", + Role = ParticipationRole.RequiredParticipant, + Rsvp = true, + ParticipationStatus = EventParticipationStatus.Tentative + }; + yield return new TestCaseData(simple).SetName("Simple attendee"); + } + + [Test] + public void Attachment_Test() + { + const string someString = "This is a string."; + var asBytes = Encoding.UTF8.GetBytes(someString); + var attachment = new Attachment { Data = asBytes, ValueEncoding = Encoding.UTF8}; + + var calendar = new Calendar(); + var vEvent = GetSimpleEvent(); + vEvent.Attachments = new List {attachment}; + calendar.Events.Add(vEvent); + + var serialized = SerializeToString(calendar); + var unserialized = UnserializeCalendar(serialized); + + Assert.AreEqual(calendar.GetHashCode(), unserialized.GetHashCode()); + Assert.AreEqual(calendar, unserialized); + } } } diff --git a/ical.NET/Components/Event.cs b/ical.NET/Components/Event.cs index 627decb41..cfdfe0d6a 100644 --- a/ical.NET/Components/Event.cs +++ b/ical.NET/Components/Event.cs @@ -293,7 +293,8 @@ protected bool Equals(Event other) && Status.Equals(other.Status) && IsActive() == other.IsActive() && Transparency.Equals(other.Transparency) - && EvaluationIncludesReferenceDate == other.EvaluationIncludesReferenceDate; + && EvaluationIncludesReferenceDate == other.EvaluationIncludesReferenceDate + && Attachments.SequenceEqual(other.Attachments); } public override bool Equals(object obj) diff --git a/ical.NET/DataTypes/Attachment.cs b/ical.NET/DataTypes/Attachment.cs index ab15408cc..2e8d9542e 100644 --- a/ical.NET/DataTypes/Attachment.cs +++ b/ical.NET/DataTypes/Attachment.cs @@ -1,8 +1,10 @@ using System; +using System.Linq; using System.Text; using Ical.Net.Interfaces.DataTypes; using Ical.Net.Interfaces.General; using Ical.Net.Serialization.iCalendar.Serializers.DataTypes; +using Utility; namespace Ical.Net.DataTypes { @@ -55,7 +57,7 @@ public override void CopyFrom(ICopyable obj) { } protected bool Equals(Attachment other) { - return Equals(Uri, other.Uri) && Equals(Data, other.Data) && Equals(ValueEncoding, other.ValueEncoding); + return Equals(Uri, other.Uri) && ValueEncoding.Equals(other.ValueEncoding) && Data.SequenceEqual(other.Data); } public override bool Equals(object obj) @@ -71,7 +73,7 @@ public override int GetHashCode() unchecked { var hashCode = Uri?.GetHashCode() ?? 0; - hashCode = (hashCode * 397) ^ (Data?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (CollectionHelpers.GetHashCode(Data)); hashCode = (hashCode * 397) ^ (ValueEncoding?.GetHashCode() ?? 0); return hashCode; } diff --git a/ical.NET/DataTypes/Attendee.cs b/ical.NET/DataTypes/Attendee.cs index 8923214de..ed41b33e7 100644 --- a/ical.NET/DataTypes/Attendee.cs +++ b/ical.NET/DataTypes/Attendee.cs @@ -1,106 +1,231 @@ using System; using System.Collections.Generic; +using System.Linq; using Ical.Net.Interfaces.DataTypes; using Ical.Net.Interfaces.General; +using Utility; namespace Ical.Net.DataTypes { public class Attendee : EncodableDataType, IAttendee { + private Uri _sentBy; + /// SENT-BY, to indicate who is acting on behalf of the ATTENDEE public virtual Uri SentBy { - get { return new Uri(Parameters.Get("SENT-BY")); } - set + get { - if (value != null) + if (_sentBy != null) { - Parameters.Set("SENT-BY", value.OriginalString); + return _sentBy; } - else + + var newUrl = Parameters.Get("SENT-BY"); + Uri.TryCreate(newUrl, UriKind.RelativeOrAbsolute, out _sentBy); + return _sentBy; + } + set + { + if (value == null || value == _sentBy) { - Parameters.Set("SENT-BY", (string) null); + return; } + _sentBy = value; + Parameters.Set("SENT-BY", value.OriginalString); } } + private string _commonName; + /// CN: to show the common or displayable name associated with the calendar address public virtual string CommonName { - get { return Parameters.Get("CN"); } - set { Parameters.Set("CN", value); } + get + { + if (string.IsNullOrEmpty(_commonName)) + { + _commonName = Parameters.Get("CN"); + } + return _commonName; + } + set + { + if (string.Equals(_commonName, value, StringComparison.OrdinalIgnoreCase)) + { + return; + } + _commonName = value; + Parameters.Set("CN", value); + } } + private Uri _directoryEntry; + /// DIR, to indicate the URI that points to the directory information corresponding to the attendee public virtual Uri DirectoryEntry { - get { return new Uri(Parameters.Get("DIR")); } - set + get { - if (value != null) + if (_directoryEntry != null) { - Parameters.Set("DIR", value.OriginalString); + return _directoryEntry; } - else + + var newUrl = Parameters.Get("SENT-BY"); + Uri.TryCreate(newUrl, UriKind.RelativeOrAbsolute, out _directoryEntry); + return _directoryEntry; + } + set + { + if (value == null || value == _directoryEntry) { - Parameters.Set("DIR", (string) null); + return; } + _directoryEntry = value; + Parameters.Set("DIR", value.OriginalString); } } + private string _type; + /// CUTYPE: the type of calendar user public virtual string Type { - get { return Parameters.Get("CUTYPE"); } - set { Parameters.Set("CUTYPE", value); } + get + { + if (string.IsNullOrEmpty(_type)) + { + _type = Parameters.Get("CUTYPE"); + } + return _type; + } + set + { + if (string.Equals(_type, value, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + _type = value; + Parameters.Set("CUTYPE", value); + } } + private List _members; + /// MEMBER: the groups the user belongs to public virtual IList Members { - get { return Parameters.GetMany("MEMBER"); } - set { Parameters.Set("MEMBER", value); } + get { return _members ?? (_members = new List(Parameters.GetMany("MEMBER"))); } + set + { + _members = new List(value); + Parameters.Set("MEMBER", value); + } } + private string _role; + /// ROLE: the intended role the attendee will have public virtual string Role { - get { return Parameters.Get("ROLE"); } - set { Parameters.Set("ROLE", value); } + get + { + if (string.IsNullOrEmpty(_role)) + { + _role = Parameters.Get("ROLE"); + } + return _role; + } + set + { + if (string.Equals(_role, value, StringComparison.OrdinalIgnoreCase)) + { + return; + } + _role = value; + Parameters.Set("ROLE", value); + } } + private string _participationStatus; public virtual string ParticipationStatus { - get { return Parameters.Get("PARTSTAT"); } - set { Parameters.Set("PARTSTAT", value); } + get + { + if (string.IsNullOrEmpty(_participationStatus)) + { + _participationStatus = Parameters.Get("PARTSTAT"); + } + return _participationStatus; + } + set + { + if (string.Equals(_participationStatus, value, StringComparison.OrdinalIgnoreCase)) + { + return; + } + _participationStatus = value; + Parameters.Set("PARTSTAT", value); + } } + private bool? _rsvp; + /// RSVP, to indicate whether a reply is requested public virtual bool Rsvp { get { + if (_rsvp != null) + { + return _rsvp.Value; + } + bool val; var rsvp = Parameters.Get("RSVP"); if (rsvp != null && bool.TryParse(rsvp, out val)) { - return val; + _rsvp = val; + return _rsvp.Value; } return false; } set { - var val = value.ToString(); - val = val.ToUpper(); + _rsvp = value; + var val = value.ToString().ToUpperInvariant(); Parameters.Set("RSVP", val); } } + private List _delegatedTo; + /// DELEGATED-TO, to indicate the calendar users that the original request was delegated to public virtual IList DelegatedTo { - get { return Parameters.GetMany("DELEGATED-TO"); } - set { Parameters.Set("DELEGATED-TO", value); } + get { return _delegatedTo ?? (_delegatedTo = new List(Parameters.GetMany("DELEGATED-TO"))); } + set + { + if (value == null) + { + return; + } + _delegatedTo = new List(value); + Parameters.Set("DELEGATED-TO", value); + } } + private List _delegatedFrom; + /// DELEGATED-FROM, to indicate whom the request was delegated from public virtual IList DelegatedFrom { - get { return Parameters.GetMany("DELEGATED-FROM"); } - set { Parameters.Set("DELEGATED-FROM", value); } + get { return _delegatedFrom ?? (_delegatedFrom = new List(Parameters.GetMany("DELEGATED-FROM"))); } + set + { + if (value == null) + { + return; + } + _delegatedFrom = new List(value); + Parameters.Set("DELEGATED-FROM", value); + } } + /// Uri associated with the attendee, typically an email address public virtual Uri Value { get; set; } public Attendee() {} @@ -119,34 +244,49 @@ public Attendee(string attendeeUri) Value = new Uri(attendeeUri); } + //ToDo: See if this can be deleted + public override void CopyFrom(ICopyable obj) {} + protected bool Equals(Attendee other) { - return Equals(Value, other.Value); + return Equals(SentBy, other.SentBy) + && string.Equals(CommonName, other.CommonName, StringComparison.OrdinalIgnoreCase) + && Equals(DirectoryEntry, other.DirectoryEntry) + && string.Equals(Type, other.Type, StringComparison.OrdinalIgnoreCase) + && string.Equals(Role, other.Role) + && string.Equals(ParticipationStatus, other.ParticipationStatus, StringComparison.OrdinalIgnoreCase) + && Rsvp == other.Rsvp + && Equals(Value, other.Value) + && Members.SequenceEqual(other.Members) + && DelegatedTo.SequenceEqual(other.DelegatedTo) + && DelegatedFrom.SequenceEqual(other.DelegatedFrom); } public override bool Equals(object obj) { - if (ReferenceEquals(null, obj)) - { - return false; - } - if (ReferenceEquals(this, obj)) - { - return true; - } - if (obj.GetType() != GetType()) - { - return false; - } + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; return Equals((Attendee) obj); } public override int GetHashCode() { - return Value?.GetHashCode() ?? 0; + unchecked + { + var hashCode = SentBy?.GetHashCode() ?? 0; + hashCode = (hashCode * 397) ^ (CommonName?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (DirectoryEntry?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Type?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCode(Members); + hashCode = (hashCode * 397) ^ (Role?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (ParticipationStatus?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ Rsvp.GetHashCode(); + hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCode(DelegatedTo); + hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCode(DelegatedFrom); + hashCode = (hashCode * 397) ^ (Value?.GetHashCode() ?? 0); + return hashCode; + } } - - //ToDo: See if this can be deleted - public override void CopyFrom(ICopyable obj) {} } } \ No newline at end of file