From ac5af487840e6958ddc3f0dc79deb4cde935384b Mon Sep 17 00:00:00 2001 From: Ademar Gonzalez Date: Thu, 24 Sep 2026 21:49:00 -0400 Subject: [PATCH] Implement the remaining test placeholders and the gaps they hid All 19 Inconclusive("Not implemented") tests now run against their RFC fixtures. 12 only needed writing. The rest exposed: - ContactUpdate threw NullReferenceException when adding or removing a status: contact:add/contact:rem was never created. It could not change authInfo (new ContactChange.AuthInfo); Password was emitted after chg, outside it, which contact-1.0 does not allow. It now goes inside chg, and email is written before disclose as RFC 5733 orders. The two Nominet privacy fixtures had been edited to match the old output; they are back to Nominet's documented form. - DomainTransfer always sent op="request". New Operation (request, query, approve, reject, cancel), Period, and AuthInfoRoid for the pw roid attribute. - Contact transfer did not exist: new ContactTransfer command and ContactTransferResponse, sharing the TransferOperation enum. - HostCreateResponse.HostCreateResult was protected; now public. Command tests compare XML structurally, so fixture formatting no longer matters. Bump to 1.9.0. Co-Authored-By: Claude Opus 5.5 --- EppLib.UnitTests/LocalTest.cs | 234 ++++++++++++------ .../ContactUpdateCommandNominetPrivacyOff.xml | 2 +- .../ContactUpdateCommandNominetPrivacyOn.xml | 2 +- EppLib/Entities/Contact/ContactChange.cs | 5 + EppLib/Entities/Contact/ContactTransfer.cs | 47 ++++ .../Contact/ContactTransferResponse.cs | 71 ++++++ EppLib/Entities/Contact/ContactUpdate.cs | 40 +-- EppLib/Entities/Domain/DomainTransfer.cs | 36 ++- EppLib/Entities/Host/HostCreateResponse.cs | 2 +- EppLib/Entities/TransferOperation.cs | 35 +++ EppLib/EppLib.csproj | 7 +- 11 files changed, 390 insertions(+), 91 deletions(-) create mode 100644 EppLib/Entities/Contact/ContactTransfer.cs create mode 100644 EppLib/Entities/Contact/ContactTransferResponse.cs create mode 100644 EppLib/Entities/TransferOperation.cs diff --git a/EppLib.UnitTests/LocalTest.cs b/EppLib.UnitTests/LocalTest.cs index e4ec0b7..aac1162 100644 --- a/EppLib.UnitTests/LocalTest.cs +++ b/EppLib.UnitTests/LocalTest.cs @@ -321,7 +321,13 @@ public void TestHostCreateCommand1() [DeploymentItem("TestData/HostCreateResponse1.xml")] public void TestHostCreateResponse1() { - Assert.Inconclusive("Not implemented"); + var response = new HostCreateResponse(File.ReadAllBytes("HostCreateResponse1.xml")); + + Assert.AreEqual("1000", response.Code); + Assert.AreEqual("ns1.example.com", response.HostCreateResult.HostName); + Assert.AreEqual("1999-04-03T22:00:00.0Z", response.HostCreateResult.CreatedDate); + Assert.AreEqual("ABC-12345", response.ClientTransactionId); + Assert.AreEqual("54322-XYZ", response.ServerTransactionId); } #endregion @@ -399,7 +405,9 @@ public void TestHostUpdateResponse1() [DeploymentItem("TestData/HostDeleteCommand1.xml")] public void TestHostDeleteCommand1() { - Assert.Inconclusive("Not implemented"); + var command = new HostDelete("ns1.example.com") { TransactionId = "ABC-12345" }; + + AssertXmlEquivalent(File.ReadAllText("HostDeleteCommand1.xml"), command.ToXml().OuterXml); } /// @@ -410,7 +418,12 @@ public void TestHostDeleteCommand1() [DeploymentItem("TestData/HostDeleteResponse1.xml")] public void TestHostDeleteResponse1() { - Assert.Inconclusive("Not implemented"); + var response = new HostDeleteResponse(File.ReadAllBytes("HostDeleteResponse1.xml")); + + Assert.AreEqual("1000", response.Code); + Assert.AreEqual("Command completed successfully", response.Message); + Assert.AreEqual("ABC-12345", response.ClientTransactionId); + Assert.AreEqual("54321-XYZ", response.ServerTransactionId); } #endregion @@ -567,7 +580,25 @@ public void TestContactInfoResponse1() [DeploymentItem("TestData/ContactUpdateCommand1.xml")] public void TestContactUpdateCommand1() { - Assert.Inconclusive("Not implemented"); + var command = new ContactUpdate("sh8013") { TransactionId = "ABC-12345" }; + command.ToAdd = new EppContactUpdateAddRemove(); + command.ToAdd.Status.Add(new Status("", "clientDeleteProhibited")); + command.ContactChange = new ContactChange + { + PostalInfo = new PostalInfo + { + m_type = "int", + m_org = "", + m_address = new PostalAddress { Street1 = "124 Example Dr.", Street2 = "Suite 200", City = "Dulles", StateProvince = "VA", PostalCode = "20166-6503", CountryCode = "US" } + }, + Voice = new Telephone("+1.7034444444", null), + Fax = new Telephone("", null), + AuthInfo = "2fooBAR", + DiscloseFlag = true, + DiscloseMask = Contact.DiscloseFlags.Voice | Contact.DiscloseFlags.Email + }; + + AssertXmlEquivalent(File.ReadAllText("ContactUpdateCommand1.xml"), command.ToXml().OuterXml); } /// @@ -578,12 +609,16 @@ public void TestContactUpdateCommand1() [DeploymentItem("TestData/ContactUpdateResponse1.xml")] public void TestContactUpdateResponse1() { - Assert.Inconclusive("Not implemented"); + var response = new ContactUpdateResponse(File.ReadAllBytes("ContactUpdateResponse1.xml")); + + Assert.AreEqual("1000", response.Code); + Assert.AreEqual("Command completed successfully", response.Message); + Assert.AreEqual("ABC-12345", response.ClientTransactionId); + Assert.AreEqual("54321-XYZ", response.ServerTransactionId); } /// /// Contact Update command, This is the Nominet specific example from their documentation for privacy off - /// NOTE: minor change to example XML, contact:disclose moved before contact:authinfo /// Disclose is set /// [TestMethod] @@ -607,7 +642,6 @@ public void TestContactUpdateCommandNominetPrivacyOff() /// /// Contact Update command, This is the Nominet specific example from their documentation for privacy on - /// NOTE: minor change to example XML, contact:disclose moved before contact:authinfo /// Disclose is not set /// [TestMethod] @@ -635,7 +669,6 @@ public void TestContactUpdateCommandNominetPrivacyOn() /// /// Contact create command, example RFC5733 - /// NOTE: minor change to example XML, contact:disclose moved before contact:authinfo /// Disclose is not set /// [TestMethod] @@ -662,7 +695,6 @@ public void TestContactCreateCommand1() /// /// Contact create command, This is the Nominet specific example from their documentation for privacy on - /// NOTE: minor change to example XML, contact:disclose moved before contact:authinfo /// Disclose is set to false /// [TestMethod] @@ -724,17 +756,9 @@ public void TestContactCreateNominetPrivacyOff() [DeploymentItem("TestData/ContactTransferQueryCommand1.xml")] public void TestContactTransferQueryCommand1() { - Assert.Inconclusive("Not implemented"); + var command = new ContactTransfer("sh8013") { Operation = TransferOperation.Query, Password = "2fooBAR", TransactionId = "ABC-12345" }; - /* - string expected = File.ReadAllText("ContactTransferQueryCommand1.xml"); - - var command = new ContactTransferQuery("sh8013"); - command.TransactionId = "ABC-12345"; - command.Password = "2fooBAR"; - - Assert.AreEqual(expected, command.ToXml().InnerXml); - */ + AssertXmlEquivalent(File.ReadAllText("ContactTransferQueryCommand1.xml"), command.ToXml().OuterXml); } /// @@ -745,27 +769,18 @@ public void TestContactTransferQueryCommand1() [DeploymentItem("TestData/ContactTransferQueryResponse1.xml")] public void TestContactTransferQueryResponse1() { - Assert.Inconclusive("Not implemented"); - - /* - byte[] input = File.ReadAllBytes("ContactTransferQueryResponse1.xml"); - var response = new ContactTransferQueryResponse(input); + var response = new ContactTransferResponse(File.ReadAllBytes("ContactTransferQueryResponse1.xml")); Assert.AreEqual("1000", response.Code); Assert.AreEqual("Command completed successfully", response.Message); - Assert.AreEqual("sh8013", response.ContactId); - Assert.AreEqual("pending", response.TransferStatus); // trStatus - - Assert.AreEqual("ClientX", response.RequestingClient); //reID - Assert.AreEqual("2000-06-06T22:00:00.0Z", response.RequestDate); //reDate - - Assert.AreEqual("ClientY", response.ActionClient); // acID - Assert.AreEqual("2000-06-11T22:00:00.0Z", response.ActionDate); //acDate - + Assert.AreEqual("pending", response.TransferStatus); + Assert.AreEqual("ClientX", response.RequestClientId); + Assert.AreEqual("2000-06-06T22:00:00.0Z", response.RequestDate); + Assert.AreEqual("ClientY", response.ActionClientId); + Assert.AreEqual("2000-06-11T22:00:00.0Z", response.ActionDate); Assert.AreEqual("ABC-12345", response.ClientTransactionId); Assert.AreEqual("54322-XYZ", response.ServerTransactionId); - */ } #endregion @@ -780,17 +795,9 @@ public void TestContactTransferQueryResponse1() [DeploymentItem("TestData/ContactTransferRequestCommand1.xml")] public void TestContactTransferRequestCommand1() { - Assert.Inconclusive("Not implemented"); - - /* - string expected = File.ReadAllText("ContactTransferRequestCommand1.xml"); + var command = new ContactTransfer("sh8013") { Password = "2fooBAR", TransactionId = "ABC-12345" }; - var command = new ContactTransfer("sh8013"); - command.TransactionId = "ABC-12345"; - command.Password = "2fooBAR"; - - Assert.AreEqual(expected, command.ToXml().InnerXml); - */ + AssertXmlEquivalent(File.ReadAllText("ContactTransferRequestCommand1.xml"), command.ToXml().OuterXml); } /// @@ -801,27 +808,16 @@ public void TestContactTransferRequestCommand1() [DeploymentItem("TestData/ContactTransferRequestResponse1.xml")] public void TestContactTransferRequestResponse1() { - Assert.Inconclusive("Not implemented"); - - /* - byte[] input = File.ReadAllBytes("ContactTransferRequestResponse1.xml"); - var response = new ContactTransferResponse(input); + var response = new ContactTransferResponse(File.ReadAllBytes("ContactTransferRequestResponse1.xml")); Assert.AreEqual("1001", response.Code); Assert.AreEqual("Command completed successfully; action pending", response.Message); - Assert.AreEqual("sh8013", response.ContactId); - Assert.AreEqual("pending", response.TransferStatus); // trStatus - - Assert.AreEqual("ClientX", response.RequestingClient); //reID - Assert.AreEqual("2000-06-08T22:00:00.0Z", response.RequestDate); //reDate - - Assert.AreEqual("ClientY", response.ActionClient); // acID - Assert.AreEqual("2000-06-13T22:00:00.0Z", response.ActionDate); //acDate - - Assert.AreEqual("ABC-12345", response.ClientTransactionId); - Assert.AreEqual("54322-XYZ", response.ServerTransactionId); - */ + Assert.AreEqual("pending", response.TransferStatus); + Assert.AreEqual("ClientX", response.RequestClientId); + Assert.AreEqual("2000-06-08T22:00:00.0Z", response.RequestDate); + Assert.AreEqual("ClientY", response.ActionClientId); + Assert.AreEqual("2000-06-13T22:00:00.0Z", response.ActionDate); } #endregion @@ -946,7 +942,9 @@ public void TestDomainReleaseResponse1() [DeploymentItem("TestData/DomainInfoCommand1.xml")] public void TestDomainInfoCommand1() { - Assert.Inconclusive("Not implemented"); + var command = new DomainInfo("example.com") { Hosts = "all", TransactionId = "ABC-12345" }; + + AssertXmlEquivalent(File.ReadAllText("DomainInfoCommand1.xml"), command.ToXml().OuterXml); } /// @@ -957,7 +955,25 @@ public void TestDomainInfoCommand1() [DeploymentItem("TestData/DomainInfoResponse1.xml")] public void TestDomainInfoResponse1() { - Assert.Inconclusive("Not implemented"); + var response = new DomainInfoResponse(File.ReadAllBytes("DomainInfoResponse1.xml")); + var domain = response.Domain; + + Assert.AreEqual("1000", response.Code); + Assert.AreEqual("example.com", domain.Name); + Assert.AreEqual("EXAMPLE1-REP", domain.Roid); + CollectionAssert.AreEqual(new[] { "ok" }, domain.Status.Select(s => s.Type).ToArray()); + Assert.AreEqual("jd1234", domain.RegistrantId); + CollectionAssert.AreEqual(new[] { "admin:sh8013", "tech:sh8013" }, domain.Contacts.Select(c => c.Type + ":" + c.Id).ToArray()); + CollectionAssert.AreEqual(new[] { "ns1.example.com", "ns1.example.net" }, domain.NameServers.ToArray()); + CollectionAssert.AreEqual(new[] { "ns1.example.com", "ns2.example.com" }, domain.Hosts.ToArray()); + Assert.AreEqual("ClientX", domain.ClId); + Assert.AreEqual("ClientY", domain.CrId); + Assert.AreEqual("1999-04-03T22:00:00.0Z", domain.CrDate); + Assert.AreEqual("ClientX", domain.UpId); + Assert.AreEqual("1999-12-03T09:00:00.0Z", domain.UpDate); + Assert.AreEqual("2005-04-03T22:00:00.0Z", domain.ExDate); + Assert.AreEqual("2000-04-08T09:00:00.0Z", domain.TrDate); + Assert.AreEqual("2fooBAR", domain.Password); } #endregion @@ -972,7 +988,16 @@ public void TestDomainInfoResponse1() [DeploymentItem("TestData/DomainUpdateCommand1.xml")] public void TestDomainUpdateCommand1() { - Assert.Inconclusive("Not implemented"); + var command = new DomainUpdate("example.com") { TransactionId = "ABC-12345" }; + command.ToAdd.NameServers.Add("ns2.example.com"); + command.ToAdd.DomainContacts.Add(new DomainContact("mak21", "tech")); + command.ToAdd.Status.Add(new Status("Payment overdue.", "clientHold") { Lang = "en" }); + command.ToRemove.NameServers.Add("ns1.example.com"); + command.ToRemove.DomainContacts.Add(new DomainContact("sh8013", "tech")); + command.ToRemove.Status.Add(new Status("", "clientUpdateProhibited")); + command.DomainChange = new DomainChange { RegistrantContactId = "sh8013", AuthInfo = "2BARfoo" }; + + AssertXmlEquivalent(File.ReadAllText("DomainUpdateCommand1.xml"), command.ToXml().OuterXml); } /// @@ -983,7 +1008,12 @@ public void TestDomainUpdateCommand1() [DeploymentItem("TestData/DomainUpdateResponse1.xml")] public void TestDomainUpdateResponse1() { - Assert.Inconclusive("Not implemented"); + var response = new DomainUpdateResponse(File.ReadAllBytes("DomainUpdateResponse1.xml")); + + Assert.AreEqual("1000", response.Code); + Assert.AreEqual("Command completed successfully", response.Message); + Assert.AreEqual("ABC-12345", response.ClientTransactionId); + Assert.AreEqual("54321-XYZ", response.ServerTransactionId); } #endregion @@ -998,7 +1028,13 @@ public void TestDomainUpdateResponse1() [DeploymentItem("TestData/DomainCreateCommand1.xml")] public void TestDomainCreateCommand1() { - Assert.Inconclusive("Not implemented"); + var command = new DomainCreate("example.com", "jd1234") { Period = new DomainPeriod(2, "y"), Password = "2fooBAR", TransactionId = "ABC-12345" }; + command.NameServers.Add("ns1.example.net"); + command.NameServers.Add("ns2.example.net"); + command.DomainContacts.Add(new DomainContact("sh8013", "admin")); + command.DomainContacts.Add(new DomainContact("sh8013", "tech")); + + AssertXmlEquivalent(File.ReadAllText("DomainCreateCommand1.xml"), command.ToXml().OuterXml); } /// @@ -1009,7 +1045,12 @@ public void TestDomainCreateCommand1() [DeploymentItem("TestData/DomainCreateResponse1.xml")] public void TestDomainCreateResponse1() { - Assert.Inconclusive("Not implemented"); + var response = new DomainCreateResponse(File.ReadAllBytes("DomainCreateResponse1.xml")); + + Assert.AreEqual("1000", response.Code); + Assert.AreEqual("example.com", response.DomainCreateResult.DomainName); + Assert.AreEqual("1999-04-03T22:00:00.0Z", response.DomainCreateResult.CreatedDate); + Assert.AreEqual("2001-04-03T22:00:00.0Z", response.DomainCreateResult.ExpirationDate); } #endregion @@ -1024,7 +1065,9 @@ public void TestDomainCreateResponse1() [DeploymentItem("TestData/DomainDeleteCommand1.xml")] public void TestDomainDeleteCommand1() { - Assert.Inconclusive("Not implemented"); + var command = new DomainDelete("example.com") { TransactionId = "ABC-12345" }; + + AssertXmlEquivalent(File.ReadAllText("DomainDeleteCommand1.xml"), command.ToXml().OuterXml); } /// @@ -1035,7 +1078,12 @@ public void TestDomainDeleteCommand1() [DeploymentItem("TestData/DomainDeleteResponse1.xml")] public void TestDomainDeleteResponse1() { - Assert.Inconclusive("Not implemented"); + var response = new DomainDeleteResponse(File.ReadAllBytes("DomainDeleteResponse1.xml")); + + Assert.AreEqual("1000", response.Code); + Assert.AreEqual("Command completed successfully", response.Message); + Assert.AreEqual("ABC-12345", response.ClientTransactionId); + Assert.AreEqual("54321-XYZ", response.ServerTransactionId); } #endregion @@ -1100,7 +1148,9 @@ public void TestDomainRenewResponse1() [DeploymentItem("TestData/DomainTransferQueryCommand1.xml")] public void TestDomainTransferQueryCommand1() { - Assert.Inconclusive("Not implemented"); + var command = new DomainTransfer("example.com") { Operation = TransferOperation.Query, Password = "2fooBAR", AuthInfoRoid = "JD1234-REP", TransactionId = "ABC-12345" }; + + AssertXmlEquivalent(File.ReadAllText("DomainTransferQueryCommand1.xml"), command.ToXml().OuterXml); } /// @@ -1136,7 +1186,9 @@ public void TestDomainTransferQueryResponse1() [DeploymentItem("TestData/DomainTransferRequestCommand1.xml")] public void TestDomainTransferRequestCommand1() { - Assert.Inconclusive("Not implemented"); + var command = new DomainTransfer("example.com") { Period = new DomainPeriod(1, "y"), Password = "2fooBAR", AuthInfoRoid = "JD1234-REP", TransactionId = "ABC-12345" }; + + AssertXmlEquivalent(File.ReadAllText("DomainTransferRequestCommand1.xml"), command.ToXml().OuterXml); } /// @@ -1260,6 +1312,46 @@ public void TestPollMsgsResponse2() #endregion + // Compares EPP documents ignoring formatting, namespace declarations, schemaLocation and attribute order. + private static void AssertXmlEquivalent(string expected, string actual) + { + var expectedXml = NormalizeXml(System.Xml.Linq.XDocument.Parse(expected.TrimStart('\uFEFF')).Root); + var actualXml = NormalizeXml(System.Xml.Linq.XDocument.Parse(actual).Root); + + Assert.IsTrue(System.Xml.Linq.XNode.DeepEquals(expectedXml, actualXml), "Expected:\n" + expectedXml + "\nActual:\n" + actualXml); + } + + private static System.Xml.Linq.XElement NormalizeXml(System.Xml.Linq.XElement element) + { + return new System.Xml.Linq.XElement(element.Name, + element.Attributes().Where(a => !a.IsNamespaceDeclaration && a.Name.LocalName != "schemaLocation").OrderBy(a => a.Name.ToString()), + element.Nodes().Select(n => n is System.Xml.Linq.XElement child ? (object)NormalizeXml(child) + : n is System.Xml.Linq.XText text && !string.IsNullOrWhiteSpace(text.Value) ? text.Value.Trim() : null).Where(n => n != null)); + } + + /// + /// Contact update removing a status and changing email together with disclose: RFC 5733 orders email before disclose. + /// + [TestMethod] + [TestCategory("LocalCommand")] + public void TestContactUpdateRemoveStatusAndEmailBeforeDisclose() + { + var command = new ContactUpdate("sh8013") { TransactionId = "ABC-12345" }; + command.ToRemove = new EppContactUpdateAddRemove(); + command.ToRemove.Status.Add(new Status("", "clientDeleteProhibited")); + command.ContactChange = new ContactChange + { + Email = "jdoe@example.com", + DiscloseFlag = false, + DiscloseMask = Contact.DiscloseFlags.Email + }; + + var xml = command.ToXml().InnerXml; + + StringAssert.Contains(xml, ""); + StringAssert.Contains(xml, "jdoe@example.com"); + } + #region Fork gaps (1.8.0) /// diff --git a/EppLib.UnitTests/TestData/ContactUpdateCommandNominetPrivacyOff.xml b/EppLib.UnitTests/TestData/ContactUpdateCommandNominetPrivacyOff.xml index c58946e..fe6d3de 100644 --- a/EppLib.UnitTests/TestData/ContactUpdateCommandNominetPrivacyOff.xml +++ b/EppLib.UnitTests/TestData/ContactUpdateCommandNominetPrivacyOff.xml @@ -1 +1 @@ -CONTACT-12342fooBARABC-12345 \ No newline at end of file +CONTACT-12342fooBARABC-12345 \ No newline at end of file diff --git a/EppLib.UnitTests/TestData/ContactUpdateCommandNominetPrivacyOn.xml b/EppLib.UnitTests/TestData/ContactUpdateCommandNominetPrivacyOn.xml index 6293569..1ef79bc 100644 --- a/EppLib.UnitTests/TestData/ContactUpdateCommandNominetPrivacyOn.xml +++ b/EppLib.UnitTests/TestData/ContactUpdateCommandNominetPrivacyOn.xml @@ -1 +1 @@ -CONTACT-12342fooBARABC-12345 \ No newline at end of file +CONTACT-12342fooBARABC-12345 \ No newline at end of file diff --git a/EppLib/Entities/Contact/ContactChange.cs b/EppLib/Entities/Contact/ContactChange.cs index eaf7eab..8e4a34f 100755 --- a/EppLib/Entities/Contact/ContactChange.cs +++ b/EppLib/Entities/Contact/ContactChange.cs @@ -19,6 +19,11 @@ public class ContactChange public Telephone Voice; public Telephone Fax; public string Email; + + /// + /// New authorization information (password) for the contact. + /// + public string AuthInfo; public bool? DiscloseFlag; public Contact.DiscloseFlags DiscloseMask; diff --git a/EppLib/Entities/Contact/ContactTransfer.cs b/EppLib/Entities/Contact/ContactTransfer.cs new file mode 100644 index 0000000..5658f67 --- /dev/null +++ b/EppLib/Entities/Contact/ContactTransfer.cs @@ -0,0 +1,47 @@ +// Copyright 2012 Code Maker Inc. (http://codemaker.net) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +using System.Xml; + +namespace EppLib.Entities +{ + /// + /// Contact transfer command (RFC 5733 section 3.2.4). Set Password to the contact's authorization + /// information; registries require it for a request and may require it for a query. + /// + public class ContactTransfer : ContactBase + { + private readonly string contactId; + + public ContactTransfer(string contactId) + { + this.contactId = contactId; + } + + public TransferOperation Operation { get; set; } = TransferOperation.Request; + + protected override XmlElement BuildCommandElement(XmlDocument doc, XmlElement commandRootElement) + { + var contactTransfer = BuildCommandElement(doc, "transfer", commandRootElement, Operation.ToEppValue()); + + AddXmlElement(doc, contactTransfer, "contact:id", contactId, namespaceUri); + + return contactTransfer; + } + + public override ContactTransferResponse FromBytes(byte[] bytes) + { + return new ContactTransferResponse(bytes); + } + } +} diff --git a/EppLib/Entities/Contact/ContactTransferResponse.cs b/EppLib/Entities/Contact/ContactTransferResponse.cs new file mode 100644 index 0000000..0304917 --- /dev/null +++ b/EppLib/Entities/Contact/ContactTransferResponse.cs @@ -0,0 +1,71 @@ +// Copyright 2012 Code Maker Inc. (http://codemaker.net) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +using System.Xml; + +namespace EppLib.Entities +{ + /// + /// Response to a contact transfer command (RFC 5733 section 3.2.4). + /// + public class ContactTransferResponse : EppResponse + { + public ContactTransferResponse(byte[] bytes) : base(bytes) { } + + public ContactTransferResponse(string xml) : base(xml) { } + + public string ContactId { get; private set; } + + /// + /// Transfer status: clientApproved, clientCancelled, clientRejected, pending, serverApproved or serverCancelled. + /// + public string TransferStatus { get; private set; } + + /// + /// The client that requested the transfer (reID). + /// + public string RequestClientId { get; private set; } + + /// + /// When the transfer was requested (reDate), as sent by the registry. + /// + public string RequestDate { get; private set; } + + /// + /// The client that should act on the transfer (acID). + /// + public string ActionClientId { get; private set; } + + /// + /// When action was completed or is due (acDate), as sent by the registry. + /// + public string ActionDate { get; private set; } + + protected override void ProcessDataNode(XmlDocument doc, XmlNamespaceManager namespaces) + { + namespaces.AddNamespace("contact", "urn:ietf:params:xml:ns:contact-1.0"); + + var trnData = doc.SelectSingleNode("/ns:epp/ns:response/ns:resData/contact:trnData", namespaces); + + if (trnData != null) + { + ContactId = trnData.SelectSingleNode("contact:id", namespaces)?.InnerText; + TransferStatus = trnData.SelectSingleNode("contact:trStatus", namespaces)?.InnerText; + RequestClientId = trnData.SelectSingleNode("contact:reID", namespaces)?.InnerText; + RequestDate = trnData.SelectSingleNode("contact:reDate", namespaces)?.InnerText; + ActionClientId = trnData.SelectSingleNode("contact:acID", namespaces)?.InnerText; + ActionDate = trnData.SelectSingleNode("contact:acDate", namespaces)?.InnerText; + } + } + } +} diff --git a/EppLib/Entities/Contact/ContactUpdate.cs b/EppLib/Entities/Contact/ContactUpdate.cs index 3c475d0..4b02a79 100755 --- a/EppLib/Entities/Contact/ContactUpdate.cs +++ b/EppLib/Entities/Contact/ContactUpdate.cs @@ -42,31 +42,34 @@ protected override XmlElement BuildCommandElement(XmlDocument doc, XmlElement co AddXmlElement(doc, contact_update, "contact:id", ContactId, namespaceUri); - var add_element = getAddRemoveElement(doc, ToAdd); + var add_element = getAddRemoveElement(doc, ToAdd, "contact:add"); if (add_element != null) { contact_update.AppendChild(add_element); } - var remove_element = getAddRemoveElement(doc, ToRemove); + var remove_element = getAddRemoveElement(doc, ToRemove, "contact:rem"); if (remove_element != null) { contact_update.AppendChild(remove_element); } - if (ContactChange != null) + // RFC 5733 puts authInfo inside chg; the Password property would otherwise land after it. + var authInfo = ContactChange?.AuthInfo ?? (string.IsNullOrWhiteSpace(Password) ? null : Password); + + if (ContactChange != null || authInfo != null) { var change_element = doc.CreateElement("contact:chg", namespaceUri); - if (ContactChange.PostalInfo != null) + if (ContactChange?.PostalInfo != null) { var xml = AddressToXml(doc, "contact:postalInfo", ContactChange.PostalInfo); change_element.AppendChild(xml); } - if (ContactChange.Voice != null) + if (ContactChange?.Voice != null) { var voice = AddXmlElement(doc, change_element, "contact:voice", ContactChange.Voice.Value, namespaceUri); @@ -76,7 +79,7 @@ protected override XmlElement BuildCommandElement(XmlDocument doc, XmlElement co } } - if (ContactChange.Fax != null) + if (ContactChange?.Fax != null) { var voice = AddXmlElement(doc, change_element, "contact:fax", ContactChange.Fax.Value, namespaceUri); @@ -86,16 +89,23 @@ protected override XmlElement BuildCommandElement(XmlDocument doc, XmlElement co } } - if (ContactChange.DiscloseFlag != null) + // RFC 5733 order: postalInfo, voice, fax, email, authInfo, disclose. + if (ContactChange?.Email != null) + { + AddXmlElement(doc, change_element, "contact:email", ContactChange.Email, namespaceUri); + } + + if (authInfo != null) + { + var authInfoElement = AddXmlElement(doc, change_element, "contact:authInfo", null, namespaceUri); + AddXmlElement(doc, authInfoElement, "contact:pw", authInfo, namespaceUri); + } + + if (ContactChange?.DiscloseFlag != null) { var disclose = DiscloseToXml(doc, ContactChange.DiscloseMask, (bool)ContactChange.DiscloseFlag); change_element.AppendChild(disclose); } - - if (ContactChange.Email != null) - { - AddXmlElement(doc, change_element, "contact:email", ContactChange.Email, namespaceUri); - } contact_update.AppendChild(change_element); } @@ -103,7 +113,7 @@ protected override XmlElement BuildCommandElement(XmlDocument doc, XmlElement co return contact_update; } - private static XmlElement getAddRemoveElement(XmlDocument doc, EppContactUpdateAddRemove addRemoveItems) + private XmlElement getAddRemoveElement(XmlDocument doc, EppContactUpdateAddRemove addRemoveItems, string tagName) { XmlElement add_remove_element = null; @@ -112,10 +122,12 @@ private static XmlElement getAddRemoveElement(XmlDocument doc, EppContactUpdateA if (addRemoveItems.Status != null && addRemoveItems.Status.Count > 0) { + add_remove_element = doc.CreateElement(tagName, namespaceUri); + foreach (var status in addRemoveItems.Status) { - var status_element = AddXmlElement(doc, add_remove_element, "contact:status", status.Value); + var status_element = AddXmlElement(doc, add_remove_element, "contact:status", status.Value, namespaceUri); status_element.SetAttribute("s", status.Type); diff --git a/EppLib/Entities/Domain/DomainTransfer.cs b/EppLib/Entities/Domain/DomainTransfer.cs index 87d2900..2ef4187 100755 --- a/EppLib/Entities/Domain/DomainTransfer.cs +++ b/EppLib/Entities/Domain/DomainTransfer.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. using System.Collections.Generic; +using System.Globalization; using System.Xml; namespace EppLib.Entities @@ -44,12 +45,45 @@ public DomainTransfer(string mName, string registrantContactId, string adminCont this.registrantContactId = registrantContactId; } + /// + /// The transfer operation; defaults to Request. + /// + public TransferOperation Operation { get; set; } = TransferOperation.Request; + + /// + /// Years or months to add to the registration when the transfer completes. Only used with Request. + /// + public DomainPeriod Period { get; set; } + + /// + /// The repository object ID of the contact whose authorization information is in Password, + /// when it is a contact's rather than the domain's (the pw roid attribute). + /// + public string AuthInfoRoid { get; set; } + protected override XmlElement BuildCommandElement(XmlDocument doc, XmlElement commandRootElement) { - var domainTransfer = BuildCommandElement(doc, "transfer", commandRootElement, "request"); + var domainTransfer = BuildCommandElement(doc, "transfer", commandRootElement, Operation.ToEppValue()); AddXmlElement(doc, domainTransfer, "domain:name", m_name, namespaceUri); + if (Period != null) + { + var period = AddXmlElement(doc, domainTransfer, "domain:period", Period.Value.ToString(CultureInfo.InvariantCulture), namespaceUri); + period.SetAttribute("unit", Period.Unit); + } + + if (!string.IsNullOrWhiteSpace(Password)) + { + var authInfo = AddXmlElement(doc, domainTransfer, "domain:authInfo", null, namespaceUri); + var pw = AddXmlElement(doc, authInfo, "domain:pw", Password, namespaceUri); + + if (AuthInfoRoid != null) + { + pw.SetAttribute("roid", AuthInfoRoid); + } + } + return domainTransfer; } diff --git a/EppLib/Entities/Host/HostCreateResponse.cs b/EppLib/Entities/Host/HostCreateResponse.cs index f63d854..4e62fe4 100755 --- a/EppLib/Entities/Host/HostCreateResponse.cs +++ b/EppLib/Entities/Host/HostCreateResponse.cs @@ -46,6 +46,6 @@ protected override void ProcessDataNode(XmlDocument doc, XmlNamespaceManager nam } } - protected HostCreateResult HostCreateResult { get; set; } + public HostCreateResult HostCreateResult { get; protected set; } } } diff --git a/EppLib/Entities/TransferOperation.cs b/EppLib/Entities/TransferOperation.cs new file mode 100644 index 0000000..a6d826a --- /dev/null +++ b/EppLib/Entities/TransferOperation.cs @@ -0,0 +1,35 @@ +// Copyright 2012 Code Maker Inc. (http://codemaker.net) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +namespace EppLib.Entities +{ + /// + /// The op attribute of an EPP transfer command (RFC 5730 section 2.9.3.4). + /// + public enum TransferOperation + { + Request, + Query, + Approve, + Reject, + Cancel + } + + internal static class TransferOperationExtensions + { + public static string ToEppValue(this TransferOperation operation) + { + return operation.ToString().ToLowerInvariant(); + } + } +} diff --git a/EppLib/EppLib.csproj b/EppLib/EppLib.csproj index 560d2c4..8b52eb5 100644 --- a/EppLib/EppLib.csproj +++ b/EppLib/EppLib.csproj @@ -4,7 +4,7 @@ netstandard2.0;net10.0 EppLib EppLib - 1.8.0 + 1.9.0 Code Maker Inc. and Contributors EppLib.NET provides a library that makes easy for registrars to interact with registries implementing the Extensible Provisioning Protocol (EPP). Includes extensions for CIRA (.CA), Nominet (.UK), IIS (.SE), Verisign Namestore, LaunchPhase and SecDNS. epp;registrar;registry;domain;rfc5730 @@ -13,7 +13,7 @@ https://github.com/CodeMakerInc/EppLib.NET.git git README.md - HostInfoResponse.Host is now public, so host info responses can be read. DomainTransferResponse reads exDate (it looked for expDate, so ExpirationDate was always null) and the transfer status, requesting and acting clients and dates. Name servers can be sent as host attributes (hostAttr, with glue addresses) through DomainCreate.NameServerAttributes and EppDomainUpdateAddRemove.NameServerAttributes, and are returned in Domain.NameServerAttributes. PollResponse.DomainName is filled from standard domain notifications and IIS notifications. Thanks to the Esselink-nu and NoorDigitalAgency forks, which found these gaps. + ContactUpdate no longer throws when adding or removing statuses, can change the contact's authorization information (ContactChange.AuthInfo, or Password, now placed inside chg), and sends email before disclose as RFC 5733 requires. DomainTransfer supports every operation (Operation: request, query, approve, reject, cancel), a Period and the pw roid attribute (AuthInfoRoid). New ContactTransfer command and ContactTransferResponse. HostCreateResponse.HostCreateResult is now public. true false false @@ -21,6 +21,9 @@ + + +