From 520356833d3db93513f9935a8c613d70739e6151 Mon Sep 17 00:00:00 2001 From: stampercasey Date: Tue, 7 Jul 2026 09:30:16 -0400 Subject: [PATCH 1/5] VAPI-3437 Add BXML verb, reusing shared SipUri type The verb hands a call off to a SIP endpoint via SIP REFER. Reuses the existing Transfer-flavored SipUri type instead of a distinct Refer-only type, with validation rejecting Transfer-only attributes (TransferAnswerUrl, Username, Uui, etc.) when a SipUri is attached to Refer. --- docs/Refer.md | 55 ++++++ .../Unit/Model/Bxml/TestRefer.cs | 137 +++++++++++++++ .../Model/Bxml/Verbs/Refer.cs | 163 ++++++++++++++++++ 3 files changed, 355 insertions(+) create mode 100644 docs/Refer.md create mode 100644 src/Bandwidth.Standard.Test/Unit/Model/Bxml/TestRefer.cs create mode 100644 src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs diff --git a/docs/Refer.md b/docs/Refer.md new file mode 100644 index 0000000..fe52c97 --- /dev/null +++ b/docs/Refer.md @@ -0,0 +1,55 @@ +# Bandwidth.Standard.Model.Bxml.Verbs.Refer + +The `` verb is used to hand off a call to a SIP endpoint via a SIP REFER. The call is transferred to the specified SIP URI, and an optional callback is sent when the transfer completes. + +For more details, see the [Bandwidth BXML Refer documentation](https://dev.bandwidth.com/docs/voice/bxml/refer.html). + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**ReferCompleteUrl** | **string** | URL to receive the `referComplete` callback when the REFER is finished. | [optional] +**ReferCompleteMethod** | **string** | HTTP method to use for the `referComplete` callback. Must be `GET` or `POST`. | [optional] [default to `POST`] +**Tag** | **string** | Optional custom string to include in callbacks. Max 256 characters. | [optional] +**SipUriElement** | [**SipUri**](SipUri.md) | The SIP URI destination for the REFER. Must start with `sip:`. This is the same `SipUri` type used by [``](Transfer.md) - see [Shared SipUri type](#shared-sipuri-type) below for which attributes are valid in each context. | + +## Shared SipUri type + +`` and `` both use the same `SipUri` type (`Bandwidth.Standard.Model.Bxml.Verbs.SipUri`) for their SIP URI child element, rather than separate types per verb. Only a subset of `SipUri`'s attributes are valid depending on which verb it's attached to: + +Attribute | Valid for `` | Valid for `` +------------ | :---: | :---: +`Uri` | Yes | Yes +`Uui` | Yes | No +`TransferAnswerUrl` | Yes | No +`TransferAnswerMethod` | Yes | No +`TransferAnswerFallbackUrl` | Yes | No +`TransferAnswerFallbackMethod` | Yes | No +`TransferDisconnectUrl` | Yes | No +`TransferDisconnectMethod` | Yes | No +`Username` | Yes | No +`Password` | Yes | No +`FallbackUsername` | Yes | No +`FallbackPassword` | Yes | No +`Tag` | Yes | No + +Assigning a `SipUri` with any of the Transfer-only attributes set to `Refer` (via `WithSipUri(SipUri)` or by setting `SipUriElement` directly) throws an `ArgumentException` naming the offending attribute(s). This check runs at the point of assignment - mutating the `SipUri` instance's attributes *after* attaching it to `Refer` is not re-validated before serialization. + +## Methods + +Name | Description +------------ | ------------- +`WithSipUri(string sipUri)` | Sets the SIP URI destination from a string. Returns the `Refer` instance for chaining. +`WithSipUri(SipUri sipUri)` | Sets the SIP URI destination from a `SipUri` object. Throws if any Transfer-only attribute is set. Returns the `Refer` instance for chaining. +`WithReferCompleteUrl(string referCompleteUrl)` | Sets the `referCompleteUrl` attribute. Returns the `Refer` instance for chaining. +`WithReferCompleteMethod(string referCompleteMethod)` | Sets the `referCompleteMethod` attribute (`GET` or `POST`). Returns the `Refer` instance for chaining. +`WithTag(string tag)` | Sets the `tag` attribute. Returns the `Refer` instance for chaining. + +## Validation + +- `SipUri.Uri` must start with `sip:` (case-insensitive). An `ArgumentException` is thrown if the value does not match. +- `SipUri` attached to `Refer` must not have any Transfer-only attribute set (see table above). An `ArgumentException` naming the offending attribute(s) is thrown otherwise. +- `ReferCompleteMethod` must be either `GET` or `POST`. An `ArgumentException` is thrown for any other value. + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/src/Bandwidth.Standard.Test/Unit/Model/Bxml/TestRefer.cs b/src/Bandwidth.Standard.Test/Unit/Model/Bxml/TestRefer.cs new file mode 100644 index 0000000..36e1f48 --- /dev/null +++ b/src/Bandwidth.Standard.Test/Unit/Model/Bxml/TestRefer.cs @@ -0,0 +1,137 @@ +using System; +using System.IO; +using System.Xml.Serialization; +using Bandwidth.Standard.Model.Bxml; +using Bandwidth.Standard.Model.Bxml.Verbs; +using Xunit; + +namespace Bandwidth.Standard.Test.Unit.Model.Bxml +{ + public class TestRefer + { + [Fact] + public void ReferRoundTripTest() + { + var expected = " sip:alice@atlanta.example.com "; + + var refer = new Refer() + .WithSipUri("sip:alice@atlanta.example.com") + .WithReferCompleteUrl("https://example.com/handleRefer") + .WithReferCompleteMethod("POST") + .WithTag("refer-tag"); + + var actual = new Response(refer).ToBXML(); + Assert.Equal(expected, actual.Replace("\n", "").Replace("\r", "")); + + const string referOnlyXml = "sip:alice@atlanta.example.com"; + var serializer = new XmlSerializer(typeof(Refer), ""); + Refer deserializedRefer; + using (var reader = new StringReader(referOnlyXml)) + { + deserializedRefer = (Refer)serializer.Deserialize(reader); + } + + Assert.Equal("sip:alice@atlanta.example.com", deserializedRefer.SipUriElement.Uri); + var roundTrip = new Response(deserializedRefer).ToBXML(); + Assert.Equal(expected, roundTrip.Replace("\n", "").Replace("\r", "")); + } + + [Fact] + public void ReferSipUriMustStartWithSipScheme() + { + var refer = new Refer(); + Assert.Throws(() => refer.WithSipUri("tel:+15551234567")); + } + + [Fact] + public void ReferInvalidMethodThrows() + { + var refer = new Refer(); + Assert.Throws(() => refer.WithReferCompleteMethod("DELETE")); + } + + [Fact] + public void ReferWithSipUriObjectOverload() + { + // Refer reuses the same SipUri type as Transfer - not a distinct Refer-only class. + var sipUri = new SipUri { Uri = "sip:alice@atlanta.example.com" }; + var refer = new Refer().WithSipUri(sipUri); + Assert.Equal("sip:alice@atlanta.example.com", refer.SipUriElement.Uri); + } + + [Fact] + public void ReferMinimalOnlySipUri() + { + var refer = new Refer().WithSipUri("sip:bob@biloxi.example.com"); + var bxml = new Response(refer).ToBXML(); + Assert.Contains("sip:bob@biloxi.example.com", bxml); + Assert.Contains("referCompleteMethod=\"POST\"", bxml); + } + + [Fact] + public void ReferMissingSipUriProducesEmptyElement() + { + var refer = new Refer(); + var bxml = new Response(refer).ToBXML(); + // Documents that missing SipUri produces output without a SipUri element + Assert.DoesNotContain("", bxml); + } + + [Fact] + public void ReferSipUriWithNullUriProducesEmptySipUriElement() + { + // Documents that a SipUri element with no URI text serializes as an empty element + var sipUri = new SipUri(); // Uri not set; required content is missing + var refer = new Refer().WithSipUri(sipUri); + var bxml = new Response(refer).ToBXML(); + Assert.Contains("", bxml); + } + + [Theory] + [InlineData(nameof(SipUri.Uui), "base64:abc")] + [InlineData(nameof(SipUri.TransferAnswerUrl), "https://example.com/answer")] + [InlineData(nameof(SipUri.TransferAnswerMethod), "POST")] + [InlineData(nameof(SipUri.TransferAnswerFallbackUrl), "https://example.com/fallback")] + [InlineData(nameof(SipUri.TransferAnswerFallbackMethod), "POST")] + [InlineData(nameof(SipUri.TransferDisconnectUrl), "https://example.com/disconnect")] + [InlineData(nameof(SipUri.TransferDisconnectMethod), "POST")] + [InlineData(nameof(SipUri.Username), "user")] + [InlineData(nameof(SipUri.Password), "pass")] + [InlineData(nameof(SipUri.FallbackUsername), "fallbackUser")] + [InlineData(nameof(SipUri.FallbackPassword), "fallbackPass")] + [InlineData(nameof(SipUri.Tag), "some-tag")] + public void ReferRejectsTransferOnlySipUriAttributes(string propertyName, string value) + { + var sipUri = new SipUri { Uri = "sip:alice@atlanta.example.com" }; + typeof(SipUri).GetProperty(propertyName).SetValue(sipUri, value); + + var ex = Assert.Throws(() => new Refer().WithSipUri(sipUri)); + Assert.Contains(propertyName, ex.Message); + } + + [Fact] + public void ReferRejectsTransferOnlySipUriAttributesViaPropertyAssignment() + { + var sipUri = new SipUri { Uri = "sip:alice@atlanta.example.com", Username = "user" }; + var refer = new Refer(); + + Assert.Throws(() => refer.SipUriElement = sipUri); + } + + [Fact] + public void TransferOnlySipUriStillAllowsFullAttributeSetOutsideRefer() + { + // Regression: the validation added for Refer must not affect Transfer's own usage of SipUri. + var sipUri = new SipUri + { + Uri = "sip:alice@atlanta.example.com", + Username = "user", + Password = "pass", + TransferAnswerUrl = "https://example.com/answer" + }; + + Assert.Equal("user", sipUri.Username); + Assert.Equal("https://example.com/answer", sipUri.TransferAnswerUrl); + } + } +} \ No newline at end of file diff --git a/src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs b/src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs new file mode 100644 index 0000000..6a276cd --- /dev/null +++ b/src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs @@ -0,0 +1,163 @@ +using Bandwidth.Standard.Model.Bxml; +using System; +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace Bandwidth.Standard.Model.Bxml.Verbs +{ + /// + /// The Refer verb is used to hand off a call to a SIP endpoint. + /// + /// + public class Refer : IVerb + { + private string referCompleteMethod; + private SipUri sipUriElement; + + /// + /// URL to receive the refer complete callback. + /// + [XmlAttribute("referCompleteUrl")] + public string ReferCompleteUrl { get; set; } + + /// + /// HTTP method to send the refer complete callback. GET or POST. Default value is POST. + /// + [XmlAttribute("referCompleteMethod")] + public string ReferCompleteMethod + { + get { return referCompleteMethod; } + set + { + if (value != null && value != "GET" && value != "POST") + { + throw new ArgumentException("ReferCompleteMethod must be either 'GET' or 'POST'."); + } + referCompleteMethod = value; + } + } + + /// + /// Optional custom string to include in callbacks. + /// + [XmlAttribute("tag")] + public string Tag { get; set; } + + /// + /// SIP URI destination for the REFER. This is the same type used by + /// - only is valid in a REFER context. + /// Setting any Transfer-only attribute (e.g. TransferAnswerUrl, Username, Uui) on the + /// SipUri assigned here throws immediately. Note: this check runs when the SipUri is + /// assigned to Refer - mutating those attributes on the SipUri instance afterward is not + /// re-validated before serialization. + /// + [XmlElement("SipUri")] + public SipUri SipUriElement + { + get { return sipUriElement; } + set + { + ValidateSipUriForRefer(value); + sipUriElement = value; + } + } + + /// + /// Initializes a new instance of the Refer class. + /// + public Refer() + { + /// + /// Explicitly set to "POST" so the attribute is always serialized in BXML output, + /// matching the server's default and making the intent explicit to consumers. + /// + ReferCompleteMethod = "POST"; + } + + /// + /// Sets the SIP URI destination from a string. + /// + public Refer WithSipUri(string sipUri) + { + SipUriElement = new SipUri { Uri = sipUri }; + return this; + } + + /// + /// Sets the SIP URI destination from a SipUri object. Only may be + /// set - Transfer-only attributes are rejected. See . + /// + public Refer WithSipUri(SipUri sipUri) + { + SipUriElement = sipUri; + return this; + } + + /// + /// Sets referCompleteUrl. + /// + public Refer WithReferCompleteUrl(string referCompleteUrl) + { + ReferCompleteUrl = referCompleteUrl; + return this; + } + + /// + /// Sets referCompleteMethod. + /// + public Refer WithReferCompleteMethod(string referCompleteMethod) + { + ReferCompleteMethod = referCompleteMethod; + return this; + } + + /// + /// Sets tag. + /// + public Refer WithTag(string tag) + { + Tag = tag; + return this; + } + + /// + /// Throws if the given SipUri has any attribute set that is only valid for <Transfer>, + /// or if Uri does not start with "sip:". Preserves the validation the old Refer-only + /// SipUri type used to perform on its own, now enforced at the point of attachment instead. + /// + private static void ValidateSipUriForRefer(SipUri sipUri) + { + if (sipUri == null) + { + return; + } + + if (sipUri.Uri != null && !sipUri.Uri.StartsWith("sip:", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException("SipUri must start with 'sip:'."); + } + + var illegalAttributes = new List(); + if (sipUri.Uui != null) illegalAttributes.Add(nameof(SipUri.Uui)); + if (sipUri.TransferAnswerUrl != null) illegalAttributes.Add(nameof(SipUri.TransferAnswerUrl)); + if (sipUri.TransferAnswerMethod != null) illegalAttributes.Add(nameof(SipUri.TransferAnswerMethod)); + if (sipUri.TransferAnswerFallbackUrl != null) illegalAttributes.Add(nameof(SipUri.TransferAnswerFallbackUrl)); + if (sipUri.TransferAnswerFallbackMethod != null) illegalAttributes.Add(nameof(SipUri.TransferAnswerFallbackMethod)); + if (sipUri.TransferDisconnectUrl != null) illegalAttributes.Add(nameof(SipUri.TransferDisconnectUrl)); + if (sipUri.TransferDisconnectMethod != null) illegalAttributes.Add(nameof(SipUri.TransferDisconnectMethod)); + if (sipUri.Username != null) illegalAttributes.Add(nameof(SipUri.Username)); + if (sipUri.Password != null) illegalAttributes.Add(nameof(SipUri.Password)); + if (sipUri.FallbackUsername != null) illegalAttributes.Add(nameof(SipUri.FallbackUsername)); + if (sipUri.FallbackPassword != null) illegalAttributes.Add(nameof(SipUri.FallbackPassword)); + if (sipUri.Tag != null) illegalAttributes.Add(nameof(SipUri.Tag)); + + if (illegalAttributes.Count > 0) + { + throw new ArgumentException( + "SipUri attached to may only set Uri. The following Transfer-only " + + $"attribute(s) are not valid in a REFER context: {string.Join(", ", illegalAttributes)}." + ); + } + } + } +} From 3b462b1ec8a04bd3c4cdeac885af4cfcf3716573 Mon Sep 17 00:00:00 2001 From: stampercasey Date: Tue, 7 Jul 2026 09:41:02 -0400 Subject: [PATCH 2/5] chore: trigger no-mistakes gate From 4fe6c5e19f85421a0a05bc6e989752bc9cd77b56 Mon Sep 17 00:00:00 2001 From: stampercasey Date: Tue, 7 Jul 2026 09:52:20 -0400 Subject: [PATCH 3/5] no-mistakes(document): fix broken links in new Refer verb docs --- docs/Refer.md | 4 ++-- src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Refer.md b/docs/Refer.md index fe52c97..7fa3352 100644 --- a/docs/Refer.md +++ b/docs/Refer.md @@ -2,7 +2,7 @@ The `` verb is used to hand off a call to a SIP endpoint via a SIP REFER. The call is transferred to the specified SIP URI, and an optional callback is sent when the transfer completes. -For more details, see the [Bandwidth BXML Refer documentation](https://dev.bandwidth.com/docs/voice/bxml/refer.html). +For more details, see the [Bandwidth BXML Refer documentation](https://dev.bandwidth.com/docs/voice/bxml/refer). ## Properties @@ -11,7 +11,7 @@ Name | Type | Description | Notes **ReferCompleteUrl** | **string** | URL to receive the `referComplete` callback when the REFER is finished. | [optional] **ReferCompleteMethod** | **string** | HTTP method to use for the `referComplete` callback. Must be `GET` or `POST`. | [optional] [default to `POST`] **Tag** | **string** | Optional custom string to include in callbacks. Max 256 characters. | [optional] -**SipUriElement** | [**SipUri**](SipUri.md) | The SIP URI destination for the REFER. Must start with `sip:`. This is the same `SipUri` type used by [``](Transfer.md) - see [Shared SipUri type](#shared-sipuri-type) below for which attributes are valid in each context. | +**SipUriElement** | [**SipUri**](#shared-sipuri-type) | The SIP URI destination for the REFER. Must start with `sip:`. This is the same `SipUri` type used by [``](https://dev.bandwidth.com/docs/voice/bxml/transfer) - see [Shared SipUri type](#shared-sipuri-type) below for which attributes are valid in each context. | ## Shared SipUri type diff --git a/src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs b/src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs index 6a276cd..2c8f36e 100644 --- a/src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs +++ b/src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs @@ -7,7 +7,7 @@ namespace Bandwidth.Standard.Model.Bxml.Verbs { /// /// The Refer verb is used to hand off a call to a SIP endpoint. - /// + /// /// public class Refer : IVerb { From 0ad38b66e74ab9bdb24e04e5c9493db39f990c40 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Tue, 7 Jul 2026 14:33:03 -0400 Subject: [PATCH 4/5] update model and test --- .../Unit/Model/Bxml/TestRefer.cs | 124 ++-------------- .../Model/Bxml/Verbs/Refer.cs | 138 ++---------------- 2 files changed, 20 insertions(+), 242 deletions(-) diff --git a/src/Bandwidth.Standard.Test/Unit/Model/Bxml/TestRefer.cs b/src/Bandwidth.Standard.Test/Unit/Model/Bxml/TestRefer.cs index 36e1f48..63ea63d 100644 --- a/src/Bandwidth.Standard.Test/Unit/Model/Bxml/TestRefer.cs +++ b/src/Bandwidth.Standard.Test/Unit/Model/Bxml/TestRefer.cs @@ -10,128 +10,22 @@ namespace Bandwidth.Standard.Test.Unit.Model.Bxml public class TestRefer { [Fact] - public void ReferRoundTripTest() + public void ReferTest() { var expected = " sip:alice@atlanta.example.com "; - var refer = new Refer() - .WithSipUri("sip:alice@atlanta.example.com") - .WithReferCompleteUrl("https://example.com/handleRefer") - .WithReferCompleteMethod("POST") - .WithTag("refer-tag"); - - var actual = new Response(refer).ToBXML(); - Assert.Equal(expected, actual.Replace("\n", "").Replace("\r", "")); - - const string referOnlyXml = "sip:alice@atlanta.example.com"; - var serializer = new XmlSerializer(typeof(Refer), ""); - Refer deserializedRefer; - using (var reader = new StringReader(referOnlyXml)) - { - deserializedRefer = (Refer)serializer.Deserialize(reader); - } - - Assert.Equal("sip:alice@atlanta.example.com", deserializedRefer.SipUriElement.Uri); - var roundTrip = new Response(deserializedRefer).ToBXML(); - Assert.Equal(expected, roundTrip.Replace("\n", "").Replace("\r", "")); - } - - [Fact] - public void ReferSipUriMustStartWithSipScheme() - { - var refer = new Refer(); - Assert.Throws(() => refer.WithSipUri("tel:+15551234567")); - } - - [Fact] - public void ReferInvalidMethodThrows() - { - var refer = new Refer(); - Assert.Throws(() => refer.WithReferCompleteMethod("DELETE")); - } - - [Fact] - public void ReferWithSipUriObjectOverload() - { - // Refer reuses the same SipUri type as Transfer - not a distinct Refer-only class. var sipUri = new SipUri { Uri = "sip:alice@atlanta.example.com" }; - var refer = new Refer().WithSipUri(sipUri); - Assert.Equal("sip:alice@atlanta.example.com", refer.SipUriElement.Uri); - } - - [Fact] - public void ReferMinimalOnlySipUri() - { - var refer = new Refer().WithSipUri("sip:bob@biloxi.example.com"); - var bxml = new Response(refer).ToBXML(); - Assert.Contains("sip:bob@biloxi.example.com", bxml); - Assert.Contains("referCompleteMethod=\"POST\"", bxml); - } - [Fact] - public void ReferMissingSipUriProducesEmptyElement() - { - var refer = new Refer(); - var bxml = new Response(refer).ToBXML(); - // Documents that missing SipUri produces output without a SipUri element - Assert.DoesNotContain("", bxml); - } - - [Fact] - public void ReferSipUriWithNullUriProducesEmptySipUriElement() - { - // Documents that a SipUri element with no URI text serializes as an empty element - var sipUri = new SipUri(); // Uri not set; required content is missing - var refer = new Refer().WithSipUri(sipUri); - var bxml = new Response(refer).ToBXML(); - Assert.Contains("", bxml); - } - - [Theory] - [InlineData(nameof(SipUri.Uui), "base64:abc")] - [InlineData(nameof(SipUri.TransferAnswerUrl), "https://example.com/answer")] - [InlineData(nameof(SipUri.TransferAnswerMethod), "POST")] - [InlineData(nameof(SipUri.TransferAnswerFallbackUrl), "https://example.com/fallback")] - [InlineData(nameof(SipUri.TransferAnswerFallbackMethod), "POST")] - [InlineData(nameof(SipUri.TransferDisconnectUrl), "https://example.com/disconnect")] - [InlineData(nameof(SipUri.TransferDisconnectMethod), "POST")] - [InlineData(nameof(SipUri.Username), "user")] - [InlineData(nameof(SipUri.Password), "pass")] - [InlineData(nameof(SipUri.FallbackUsername), "fallbackUser")] - [InlineData(nameof(SipUri.FallbackPassword), "fallbackPass")] - [InlineData(nameof(SipUri.Tag), "some-tag")] - public void ReferRejectsTransferOnlySipUriAttributes(string propertyName, string value) - { - var sipUri = new SipUri { Uri = "sip:alice@atlanta.example.com" }; - typeof(SipUri).GetProperty(propertyName).SetValue(sipUri, value); - - var ex = Assert.Throws(() => new Refer().WithSipUri(sipUri)); - Assert.Contains(propertyName, ex.Message); - } - - [Fact] - public void ReferRejectsTransferOnlySipUriAttributesViaPropertyAssignment() - { - var sipUri = new SipUri { Uri = "sip:alice@atlanta.example.com", Username = "user" }; - var refer = new Refer(); - - Assert.Throws(() => refer.SipUriElement = sipUri); - } - - [Fact] - public void TransferOnlySipUriStillAllowsFullAttributeSetOutsideRefer() - { - // Regression: the validation added for Refer must not affect Transfer's own usage of SipUri. - var sipUri = new SipUri + var refer = new Refer { - Uri = "sip:alice@atlanta.example.com", - Username = "user", - Password = "pass", - TransferAnswerUrl = "https://example.com/answer" + SipUri = sipUri, + ReferCompleteUrl = "https://example.com/handleRefer", + ReferCompleteMethod = "POST", + Tag = "refer-tag" }; - Assert.Equal("user", sipUri.Username); - Assert.Equal("https://example.com/answer", sipUri.TransferAnswerUrl); + var actual = new Response(refer).ToBXML(); + Assert.Equal(expected, actual.Replace("\n", "").Replace("\r", "")); } } -} \ No newline at end of file +} diff --git a/src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs b/src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs index 2c8f36e..f7c9912 100644 --- a/src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs +++ b/src/Bandwidth.Standard/Model/Bxml/Verbs/Refer.cs @@ -11,8 +11,14 @@ namespace Bandwidth.Standard.Model.Bxml.Verbs /// public class Refer : IVerb { - private string referCompleteMethod; - private SipUri sipUriElement; + + /// + /// Initializes a new instance of the Refer class with defaults. + /// + public Refer() + { + ReferCompleteMethod = "POST"; + } /// /// URL to receive the refer complete callback. @@ -24,18 +30,7 @@ public class Refer : IVerb /// HTTP method to send the refer complete callback. GET or POST. Default value is POST. /// [XmlAttribute("referCompleteMethod")] - public string ReferCompleteMethod - { - get { return referCompleteMethod; } - set - { - if (value != null && value != "GET" && value != "POST") - { - throw new ArgumentException("ReferCompleteMethod must be either 'GET' or 'POST'."); - } - referCompleteMethod = value; - } - } + public string ReferCompleteMethod { get; set; } /// /// Optional custom string to include in callbacks. @@ -44,120 +39,9 @@ public string ReferCompleteMethod public string Tag { get; set; } /// - /// SIP URI destination for the REFER. This is the same type used by - /// - only is valid in a REFER context. - /// Setting any Transfer-only attribute (e.g. TransferAnswerUrl, Username, Uui) on the - /// SipUri assigned here throws immediately. Note: this check runs when the SipUri is - /// assigned to Refer - mutating those attributes on the SipUri instance afterward is not - /// re-validated before serialization. + /// A SIP URI specifying the refer destination /// [XmlElement("SipUri")] - public SipUri SipUriElement - { - get { return sipUriElement; } - set - { - ValidateSipUriForRefer(value); - sipUriElement = value; - } - } - - /// - /// Initializes a new instance of the Refer class. - /// - public Refer() - { - /// - /// Explicitly set to "POST" so the attribute is always serialized in BXML output, - /// matching the server's default and making the intent explicit to consumers. - /// - ReferCompleteMethod = "POST"; - } - - /// - /// Sets the SIP URI destination from a string. - /// - public Refer WithSipUri(string sipUri) - { - SipUriElement = new SipUri { Uri = sipUri }; - return this; - } - - /// - /// Sets the SIP URI destination from a SipUri object. Only may be - /// set - Transfer-only attributes are rejected. See . - /// - public Refer WithSipUri(SipUri sipUri) - { - SipUriElement = sipUri; - return this; - } - - /// - /// Sets referCompleteUrl. - /// - public Refer WithReferCompleteUrl(string referCompleteUrl) - { - ReferCompleteUrl = referCompleteUrl; - return this; - } - - /// - /// Sets referCompleteMethod. - /// - public Refer WithReferCompleteMethod(string referCompleteMethod) - { - ReferCompleteMethod = referCompleteMethod; - return this; - } - - /// - /// Sets tag. - /// - public Refer WithTag(string tag) - { - Tag = tag; - return this; - } - - /// - /// Throws if the given SipUri has any attribute set that is only valid for <Transfer>, - /// or if Uri does not start with "sip:". Preserves the validation the old Refer-only - /// SipUri type used to perform on its own, now enforced at the point of attachment instead. - /// - private static void ValidateSipUriForRefer(SipUri sipUri) - { - if (sipUri == null) - { - return; - } - - if (sipUri.Uri != null && !sipUri.Uri.StartsWith("sip:", StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException("SipUri must start with 'sip:'."); - } - - var illegalAttributes = new List(); - if (sipUri.Uui != null) illegalAttributes.Add(nameof(SipUri.Uui)); - if (sipUri.TransferAnswerUrl != null) illegalAttributes.Add(nameof(SipUri.TransferAnswerUrl)); - if (sipUri.TransferAnswerMethod != null) illegalAttributes.Add(nameof(SipUri.TransferAnswerMethod)); - if (sipUri.TransferAnswerFallbackUrl != null) illegalAttributes.Add(nameof(SipUri.TransferAnswerFallbackUrl)); - if (sipUri.TransferAnswerFallbackMethod != null) illegalAttributes.Add(nameof(SipUri.TransferAnswerFallbackMethod)); - if (sipUri.TransferDisconnectUrl != null) illegalAttributes.Add(nameof(SipUri.TransferDisconnectUrl)); - if (sipUri.TransferDisconnectMethod != null) illegalAttributes.Add(nameof(SipUri.TransferDisconnectMethod)); - if (sipUri.Username != null) illegalAttributes.Add(nameof(SipUri.Username)); - if (sipUri.Password != null) illegalAttributes.Add(nameof(SipUri.Password)); - if (sipUri.FallbackUsername != null) illegalAttributes.Add(nameof(SipUri.FallbackUsername)); - if (sipUri.FallbackPassword != null) illegalAttributes.Add(nameof(SipUri.FallbackPassword)); - if (sipUri.Tag != null) illegalAttributes.Add(nameof(SipUri.Tag)); - - if (illegalAttributes.Count > 0) - { - throw new ArgumentException( - "SipUri attached to may only set Uri. The following Transfer-only " + - $"attribute(s) are not valid in a REFER context: {string.Join(", ", illegalAttributes)}." - ); - } - } + public SipUri SipUri { get; set; } } } From f041ff4756a9d0c4b38a4d0ad5a02acf141b9767 Mon Sep 17 00:00:00 2001 From: stampercasey Date: Thu, 9 Jul 2026 16:55:50 -0400 Subject: [PATCH 5/5] VAPI-3437 delete refer.md and update sipuri docs --- docs/Refer.md | 55 ------------------- .../Model/Bxml/Verbs/SipUri.cs | 5 +- 2 files changed, 2 insertions(+), 58 deletions(-) delete mode 100644 docs/Refer.md diff --git a/docs/Refer.md b/docs/Refer.md deleted file mode 100644 index 7fa3352..0000000 --- a/docs/Refer.md +++ /dev/null @@ -1,55 +0,0 @@ -# Bandwidth.Standard.Model.Bxml.Verbs.Refer - -The `` verb is used to hand off a call to a SIP endpoint via a SIP REFER. The call is transferred to the specified SIP URI, and an optional callback is sent when the transfer completes. - -For more details, see the [Bandwidth BXML Refer documentation](https://dev.bandwidth.com/docs/voice/bxml/refer). - -## Properties - -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**ReferCompleteUrl** | **string** | URL to receive the `referComplete` callback when the REFER is finished. | [optional] -**ReferCompleteMethod** | **string** | HTTP method to use for the `referComplete` callback. Must be `GET` or `POST`. | [optional] [default to `POST`] -**Tag** | **string** | Optional custom string to include in callbacks. Max 256 characters. | [optional] -**SipUriElement** | [**SipUri**](#shared-sipuri-type) | The SIP URI destination for the REFER. Must start with `sip:`. This is the same `SipUri` type used by [``](https://dev.bandwidth.com/docs/voice/bxml/transfer) - see [Shared SipUri type](#shared-sipuri-type) below for which attributes are valid in each context. | - -## Shared SipUri type - -`` and `` both use the same `SipUri` type (`Bandwidth.Standard.Model.Bxml.Verbs.SipUri`) for their SIP URI child element, rather than separate types per verb. Only a subset of `SipUri`'s attributes are valid depending on which verb it's attached to: - -Attribute | Valid for `` | Valid for `` ------------- | :---: | :---: -`Uri` | Yes | Yes -`Uui` | Yes | No -`TransferAnswerUrl` | Yes | No -`TransferAnswerMethod` | Yes | No -`TransferAnswerFallbackUrl` | Yes | No -`TransferAnswerFallbackMethod` | Yes | No -`TransferDisconnectUrl` | Yes | No -`TransferDisconnectMethod` | Yes | No -`Username` | Yes | No -`Password` | Yes | No -`FallbackUsername` | Yes | No -`FallbackPassword` | Yes | No -`Tag` | Yes | No - -Assigning a `SipUri` with any of the Transfer-only attributes set to `Refer` (via `WithSipUri(SipUri)` or by setting `SipUriElement` directly) throws an `ArgumentException` naming the offending attribute(s). This check runs at the point of assignment - mutating the `SipUri` instance's attributes *after* attaching it to `Refer` is not re-validated before serialization. - -## Methods - -Name | Description ------------- | ------------- -`WithSipUri(string sipUri)` | Sets the SIP URI destination from a string. Returns the `Refer` instance for chaining. -`WithSipUri(SipUri sipUri)` | Sets the SIP URI destination from a `SipUri` object. Throws if any Transfer-only attribute is set. Returns the `Refer` instance for chaining. -`WithReferCompleteUrl(string referCompleteUrl)` | Sets the `referCompleteUrl` attribute. Returns the `Refer` instance for chaining. -`WithReferCompleteMethod(string referCompleteMethod)` | Sets the `referCompleteMethod` attribute (`GET` or `POST`). Returns the `Refer` instance for chaining. -`WithTag(string tag)` | Sets the `tag` attribute. Returns the `Refer` instance for chaining. - -## Validation - -- `SipUri.Uri` must start with `sip:` (case-insensitive). An `ArgumentException` is thrown if the value does not match. -- `SipUri` attached to `Refer` must not have any Transfer-only attribute set (see table above). An `ArgumentException` naming the offending attribute(s) is thrown otherwise. -- `ReferCompleteMethod` must be either `GET` or `POST`. An `ArgumentException` is thrown for any other value. - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - diff --git a/src/Bandwidth.Standard/Model/Bxml/Verbs/SipUri.cs b/src/Bandwidth.Standard/Model/Bxml/Verbs/SipUri.cs index 9a2d198..f57e48d 100644 --- a/src/Bandwidth.Standard/Model/Bxml/Verbs/SipUri.cs +++ b/src/Bandwidth.Standard/Model/Bxml/Verbs/SipUri.cs @@ -4,8 +4,7 @@ namespace Bandwidth.Standard.Model.Bxml.Verbs { /// - /// BXML tag to represent a SIP URI for the transfer verb. - /// + /// BXML tag to represent a SIP URI for the transfer or refer verb. /// public class SipUri : IVerb { @@ -16,7 +15,7 @@ public class SipUri : IVerb public string Uri { get; set; } /// - /// (optional) The value of the User-To-User header to send within the initial INVITE. + /// (optional, transfer only) The value of the User-To-User header to send within the initial INVITE. /// [XmlAttribute("uui")] public string Uui { get; set; }