From e71f6383c5afa23a11feef09cb56433ca614369d Mon Sep 17 00:00:00 2001 From: Chris Smola Date: Mon, 29 Jan 2024 17:26:36 -0500 Subject: [PATCH] adds new revrec performance obligation entity with tests --- CONTRIBUTING.md | 41 ++++++ Library/Client.cs | 1 + Library/Configuration/Settings.cs | 14 +- Library/List/PerformanceObligationList.cs | 44 +++++++ Library/PerformanceObligation.cs | 123 ++++++++++++++++++ Test/Fixtures/FixtureImporter.cs | 2 + .../performance_obligations/index-200.xml | 42 ++++++ .../performance_obligations/show-200.xml | 10 ++ Test/List/PerformanceObligationListTest.cs | 35 +++++ Test/PerformanceObligationTest.cs | 27 ++++ Test/Recurly.Test.csproj | 6 + 11 files changed, 342 insertions(+), 3 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 Library/List/PerformanceObligationList.cs create mode 100644 Library/PerformanceObligation.cs create mode 100644 Test/Fixtures/performance_obligations/index-200.xml create mode 100644 Test/Fixtures/performance_obligations/show-200.xml create mode 100644 Test/List/PerformanceObligationListTest.cs create mode 100644 Test/PerformanceObligationTest.cs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..85ff9d48 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,41 @@ +# Contributing + +## Adding a new entity + +If you are adding a new entity, it is usually easier to begin with an existing entity and modify it to suit your needs. Choose a "fully-featured" entity (i.e. all CRUD operations, including a `*List` class) to start as it will be easier to delete methods rather than add methods manually. As you edit, keep the following in mind: + +- If a class member shouldn't be fully accessible (i.e. `public`), then specify the access modifier (i.e. `protected` or `private`) and specify the getter/setter as appropriate (e.g. `public string FirstName { get; private set; }` restricts the value to be set internally, such as when the response XML is parsed). +- If properties can be set on create but remain readonly afterward, you will likely need another `WriteXml()` method (e.g. `WriteUpdateXml()`) that you will then need to reference in your `Update()` method. +- Decide the flexibility of multiple constructors. _Every_ entity should have the empty constructor, but you may want to add additional constructors for convenience. For example, `public Account(string accountCode)` is useful for fetching an account from the API, but `public Account(string firstName, string lastName)` is not useful because you can't create an account without an account code. + +### Lists + +If your entity supports an index/list endpoint, then you need to create an additional model in the `Library/List/` folder. Again, it is easier to copy/paste from an existing list and modify it to suit your needs. If your endpoint supports pagination, then ensure all of the pagination methods, along with constructors that accept `filterCriteria`, are included in your new model. Additionally, if your endpoint supports filtering with custom query params, you will likely want to define an additional constructor (or two if you want the option to omit the pagination `filterCriteria`). + + +## Updating an existing entity + +If you are updating an existing entity, the process is less intensive. Generally, you will need to add the new class member(s) to the model and modify your `ReadXml()`/`WriteXml()` methods to read/write the new fields, respectively. + + +## Testing + +### Unit tests + +You should always strive to write unit tests for your changes. If you are adding a new entity, you should use a fixture in your unit tests to ensure that the XML parsing is correct. There are a lot of tests that are actually calling out to a site in production, but that pattern should be reversed for obvious reasons. + +To add your new unit test and fixture: + +1. Add your fixture(s) in `Test/Fixtures/`. This usually includes at least a `show-200.xml` and `index-200.xml` file. +2. Register the fixtures within the appropriate item group in `Test/Recurly.Test.csproj`. +3. Make the code aware of the new fixture(s) by registering your feature in `Test/Fixtures/FixtureImporter.cs`. Follow the existing pattern for already-registered fixtures. +4. In your unit test, load the fixture into your model to test the XML parsing (e.g. for a "get one" endpoint): +```csharp +var myFeature = new MyFeature(); + +var xmlFixture = FixtureImporter.Get(FixtureType.MyFeature, "show-200").Xml; +XmlTextReader reader = new XmlTextReader(new System.IO.StringReader(xmlFixture)); +myFeature.ReadXml(reader); + +myFeature.Id.Should().Be("abc123"); +``` diff --git a/Library/Client.cs b/Library/Client.cs index cad44a03..2b449eec 100644 --- a/Library/Client.cs +++ b/Library/Client.cs @@ -6,6 +6,7 @@ using System.Xml; using Recurly.Configuration; +[assembly: InternalsVisibleTo("TestRig")] [assembly: InternalsVisibleTo("Recurly.Test")] namespace Recurly diff --git a/Library/Configuration/Settings.cs b/Library/Configuration/Settings.cs index 52d4cfcd..f7448dab 100644 --- a/Library/Configuration/Settings.cs +++ b/Library/Configuration/Settings.cs @@ -77,9 +77,17 @@ public int? RequestTimeoutMilliseconds private set { _requestTimeoutMilliseconds = value; } } - protected const string RecurlyServerUri = "https://{0}.recurly.com/v2{1}"; - public const string RecurlyApiVersion = "2.99"; - public const string ValidDomain = ".recurly.com"; + protected string RecurlyServerUri + { + get + { + var portSuffix = Port != null ? $":{Port}" : string.Empty; + return "https://{0}" + ValidDomain + portSuffix + "/v2{1}"; + } + } + public string RecurlyApiVersion = "2.29"; + public static string ValidDomain = ".recurly.com"; + public static int? Port { get; set; } // static, unlikely to change public string UserAgent diff --git a/Library/List/PerformanceObligationList.cs b/Library/List/PerformanceObligationList.cs new file mode 100644 index 00000000..21363aa9 --- /dev/null +++ b/Library/List/PerformanceObligationList.cs @@ -0,0 +1,44 @@ +using System.Xml; + +namespace Recurly +{ + public class PerformanceObligationList : RecurlyList + { + internal PerformanceObligationList() + { + } + + internal PerformanceObligationList(string baseUrl) : base(Client.HttpRequestMethod.Get, baseUrl) + { + } + + public override RecurlyList Start + { + get { return HasStartPage() ? new PerformanceObligationList(StartUrl) : RecurlyList.Empty(); } + } + + public override RecurlyList Next + { + get { return HasNextPage() ? new PerformanceObligationList(NextUrl) : RecurlyList.Empty(); } + } + + public override RecurlyList Prev + { + get { return HasPrevPage() ? new PerformanceObligationList(PrevUrl) : RecurlyList.Empty(); } + } + + internal override void ReadXml(XmlTextReader reader) + { + while (reader.Read()) + { + if (reader.Name == "performance_obligations" && reader.NodeType == XmlNodeType.EndElement) + break; + + if (reader.NodeType == XmlNodeType.Element && reader.Name == "performance_obligation") + { + Add(new PerformanceObligation(reader)); + } + } + } + } +} diff --git a/Library/PerformanceObligation.cs b/Library/PerformanceObligation.cs new file mode 100644 index 00000000..043caaf1 --- /dev/null +++ b/Library/PerformanceObligation.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Xml; + +namespace Recurly +{ + + /// + /// A general ledger account in Recurly. + /// + /// + public class PerformanceObligation : RecurlyEntity + { + public string Id { get; private set; } + + public string Name { get; private set; } + + public DateTime? CreatedAt { get; private set; } + + public DateTime? UpdatedAt { get; private set; } + + internal const string UrlPrefix = "/performance_obligations/"; + + #region Constructors + + public PerformanceObligation() + { + } + + internal PerformanceObligation(XmlTextReader xmlReader) + { + ReadXml(xmlReader); + } + + #endregion + + internal override void ReadXml(XmlTextReader reader) + { + while (reader.Read()) + { + DateTime dateVal; + + if (reader.Name == "performance_obligation" && reader.NodeType == XmlNodeType.EndElement) + break; + + if (reader.NodeType != XmlNodeType.Element) continue; + + switch (reader.Name) + { + case "id": + Id = reader.ReadElementContentAsString(); + break; + + case "name": + Name = reader.ReadElementContentAsString(); + break; + + case "created_at": + if (DateTime.TryParse(reader.ReadElementContentAsString(), out dateVal)) + { + CreatedAt = dateVal; + } + break; + + case "updated_at": + if (DateTime.TryParse(reader.ReadElementContentAsString(), out dateVal)) + { + UpdatedAt = dateVal; + } + break; + } + } + } + + internal override void WriteXml(XmlTextWriter xmlWriter) + { + xmlWriter.WriteStartElement("performance_obligation"); + + xmlWriter.WriteElementString("name", Name); + + xmlWriter.WriteEndElement(); + } + } + + public sealed class PerformanceObligations + { + internal const string UrlPrefix = "/performance_obligations/"; + + /// + /// Retrieves a list of all general ledger accounts. + /// + /// + public static RecurlyList List() + { + return List(null); + } + + public static RecurlyList List(FilterCriteria filter) + { + filter = filter == null ? FilterCriteria.Instance : filter; + return new PerformanceObligationList(PerformanceObligation.UrlPrefix + "?" + filter.ToNamedValueCollection().ToString()); + } + + public static PerformanceObligation Get(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return null; + } + + var pob = new PerformanceObligation(); + + var statusCode = Client.Instance.PerformRequest(Client.HttpRequestMethod.Get, + UrlPrefix + Uri.EscapeDataString(name), + pob.ReadXml); + + return statusCode == HttpStatusCode.NotFound ? null : pob; + } + + } + +} diff --git a/Test/Fixtures/FixtureImporter.cs b/Test/Fixtures/FixtureImporter.cs index 53842bfa..c2d70ab5 100644 --- a/Test/Fixtures/FixtureImporter.cs +++ b/Test/Fixtures/FixtureImporter.cs @@ -82,5 +82,7 @@ public enum FixtureType ExternalInvoices, [Description("general_ledger_accounts")] GeneralLedgerAccounts, + [Description("performance_obligations")] + PerformanceObligations, } } diff --git a/Test/Fixtures/performance_obligations/index-200.xml b/Test/Fixtures/performance_obligations/index-200.xml new file mode 100644 index 00000000..fc2e4d39 --- /dev/null +++ b/Test/Fixtures/performance_obligations/index-200.xml @@ -0,0 +1,42 @@ +HTTP/1.1 200 OK +Content-Type: application/xml; charset=utf-8 + + + + + 1 + Material Right + 2024-01-29T23:02:30Z + 2024-01-29T23:02:30Z + + + 2 + Manual Journal + 2024-01-29T23:02:30Z + 2024-01-29T23:02:30Z + + + 3 + Manually Recognize + 2024-01-29T23:02:30Z + 2024-01-29T23:02:30Z + + + 4 + Point in Time + 2024-01-29T23:02:30Z + 2024-01-29T23:02:30Z + + + 5 + Over Time (Partial Monthly) + 2024-01-29T23:02:30Z + 2024-01-29T23:02:30Z + + + 6 + Over Time (Daily) + 2024-01-29T23:02:30Z + 2024-01-29T23:02:30Z + + diff --git a/Test/Fixtures/performance_obligations/show-200.xml b/Test/Fixtures/performance_obligations/show-200.xml new file mode 100644 index 00000000..b876ee70 --- /dev/null +++ b/Test/Fixtures/performance_obligations/show-200.xml @@ -0,0 +1,10 @@ +HTTP/1.1 200 OK +Content-Type: application/xml; charset=utf-8 + + + + 6 + Over Time (Daily) + 2024-01-29T23:02:30Z + 2024-01-29T23:02:30Z + diff --git a/Test/List/PerformanceObligationListTest.cs b/Test/List/PerformanceObligationListTest.cs new file mode 100644 index 00000000..3af37927 --- /dev/null +++ b/Test/List/PerformanceObligationListTest.cs @@ -0,0 +1,35 @@ +using System; +using System.Xml; +using FluentAssertions; +using Recurly.Test.Fixtures; + +namespace Recurly.Test +{ + public class PerformanceObligationListTest : BaseTest + { + [RecurlyFact(TestEnvironment.Type.Integration)] + public void List() + { + var pobs = new PerformanceObligationList(); + + var xmlFixture = FixtureImporter.Get(FixtureType.PerformanceObligations, "index-200").Xml; + XmlTextReader reader = new XmlTextReader(new System.IO.StringReader(xmlFixture)); + pobs.ReadXml(reader); + + pobs.Should().HaveCount(6); + + var firstPob = pobs[0]; + var secondPob = pobs[1]; + + firstPob.Id.Should().Be("1"); + firstPob.Name.Should().Be("Material Right"); + firstPob.CreatedAt.Should().Be(new DateTime(2024, 1, 29, 23, 2, 30, DateTimeKind.Utc)); + firstPob.UpdatedAt.Should().Be(new DateTime(2024, 1, 29, 23, 2, 30, DateTimeKind.Utc)); + + secondPob.Id.Should().Be("2"); + secondPob.Name.Should().Be("Manual Journal"); + secondPob.CreatedAt.Should().Be(new DateTime(2024, 1, 29, 23, 2, 30, DateTimeKind.Utc)); + secondPob.UpdatedAt.Should().Be(new DateTime(2024, 1, 29, 23, 2, 30, DateTimeKind.Utc)); + } + } +} diff --git a/Test/PerformanceObligationTest.cs b/Test/PerformanceObligationTest.cs new file mode 100644 index 00000000..dceb988a --- /dev/null +++ b/Test/PerformanceObligationTest.cs @@ -0,0 +1,27 @@ +using System; +using System.Xml; +using FluentAssertions; + +using Recurly.Test.Fixtures; + + +namespace Recurly.Test +{ + public class PerformanceObligationTest : BaseTest + { + [RecurlyFact(TestEnvironment.Type.Integration)] + public void GetPerformanceObligation() + { + var pob = new PerformanceObligation(); + + var xmlFixture = FixtureImporter.Get(FixtureType.PerformanceObligations, "show-200").Xml; + XmlTextReader reader = new XmlTextReader(new System.IO.StringReader(xmlFixture)); + pob.ReadXml(reader); + + pob.Id.Should().Be("6"); + pob.Name.Should().Be("Over Time (Daily)"); + pob.CreatedAt.Should().Be(new DateTime(2024, 1, 29, 23, 2, 30, DateTimeKind.Utc)); + pob.UpdatedAt.Should().Be(new DateTime(2024, 1, 29, 23, 2, 30, DateTimeKind.Utc)); + } + } +} diff --git a/Test/Recurly.Test.csproj b/Test/Recurly.Test.csproj index c7156640..15f95c7e 100644 --- a/Test/Recurly.Test.csproj +++ b/Test/Recurly.Test.csproj @@ -185,6 +185,12 @@ Always + + Always + + + Always + Always