From 6b4c2f4a1ee41cfe1ee9ac9a3893f245180057ff Mon Sep 17 00:00:00 2001 From: sfdrogojan <42441166+sfdrogojan@users.noreply.github.com> Date: Mon, 19 Nov 2018 14:36:51 +0200 Subject: [PATCH] TSE Feature Support - Added support for TSE - Removed Security Token in URL - Added User Agent --- .gitignore | 1 + FuelSDK-CSharp/AuthEndpointUriBuilder.cs | 32 +++++ FuelSDK-CSharp/ConfigUtil.cs | 12 ++ FuelSDK-CSharp/DefaultEndpoints.cs | 23 ++++ FuelSDK-CSharp/ETCampaign.cs | 2 +- FuelSDK-CSharp/ETCampaignAsset.cs | 2 +- FuelSDK-CSharp/ETClient.cs | 77 ++++++++---- FuelSDK-CSharp/ETEndpoint.cs | 3 +- FuelSDK-CSharp/FuelReturn.cs | 30 ++--- FuelSDK-CSharp/FuelSDK-CSharp.csproj | 5 + FuelSDK-CSharp/FuelSDKConfigurationSection.cs | 43 ++++++- FuelSDK-CSharp/StackKey.cs | 38 ++++++ FuelSDK-CSharp/UserInfo.cs | 39 ++++++ FuelSDK-Test/AuthEndpointUriBuilderTest.cs | 52 ++++++++ .../ConfigFiles/allPropertiesSet.config | 7 ++ ...PropertiesSetButAuthEndpointIsEmpty.config | 7 ++ ...PropertiesSetButRestEndpointIsEmpty.config | 7 ++ ...authEndpointMissingLegacyQueryParam.config | 7 ++ .../authEndpointWithLegacyQueryParam.config | 7 ++ ...pleQueryParamsButMissingLegacyParam.config | 7 ++ ...ipleQueryParamsIncludingLegacyParam.config | 7 ++ FuelSDK-Test/ConfigFiles/empty.config | 3 + ...missingRequiredAppSignatureProperty.config | 7 ++ .../missingRequiredClientIdProperty.config | 7 ++ ...missingRequiredClientSecretProperty.config | 7 ++ .../ConfigFiles/requiredPropertiesOnly.config | 7 ++ FuelSDK-Test/CustomConfigSectionBasedTest.cs | 34 ++++++ FuelSDK-Test/ETClientTest.cs | 42 +++++++ FuelSDK-Test/FuelSDK.Test.csproj | 48 ++++++++ .../FuelSDKConfigurationSectionTest.cs | 112 ++++++++++++++++++ FuelSDK-Test/StackKeyTest.cs | 19 +++ README.md | 30 ++++- nuspecs/FuelSDK-CSharp.nuspec | 4 +- 33 files changed, 676 insertions(+), 52 deletions(-) create mode 100644 FuelSDK-CSharp/AuthEndpointUriBuilder.cs create mode 100644 FuelSDK-CSharp/ConfigUtil.cs create mode 100644 FuelSDK-CSharp/DefaultEndpoints.cs create mode 100644 FuelSDK-CSharp/StackKey.cs create mode 100644 FuelSDK-CSharp/UserInfo.cs create mode 100644 FuelSDK-Test/AuthEndpointUriBuilderTest.cs create mode 100644 FuelSDK-Test/ConfigFiles/allPropertiesSet.config create mode 100644 FuelSDK-Test/ConfigFiles/allPropertiesSetButAuthEndpointIsEmpty.config create mode 100644 FuelSDK-Test/ConfigFiles/allPropertiesSetButRestEndpointIsEmpty.config create mode 100644 FuelSDK-Test/ConfigFiles/authEndpointMissingLegacyQueryParam.config create mode 100644 FuelSDK-Test/ConfigFiles/authEndpointWithLegacyQueryParam.config create mode 100644 FuelSDK-Test/ConfigFiles/authEndpointWithMultipleQueryParamsButMissingLegacyParam.config create mode 100644 FuelSDK-Test/ConfigFiles/authEndpointWithMultipleQueryParamsIncludingLegacyParam.config create mode 100644 FuelSDK-Test/ConfigFiles/empty.config create mode 100644 FuelSDK-Test/ConfigFiles/missingRequiredAppSignatureProperty.config create mode 100644 FuelSDK-Test/ConfigFiles/missingRequiredClientIdProperty.config create mode 100644 FuelSDK-Test/ConfigFiles/missingRequiredClientSecretProperty.config create mode 100644 FuelSDK-Test/ConfigFiles/requiredPropertiesOnly.config create mode 100644 FuelSDK-Test/CustomConfigSectionBasedTest.cs create mode 100644 FuelSDK-Test/ETClientTest.cs create mode 100644 FuelSDK-Test/FuelSDKConfigurationSectionTest.cs create mode 100644 FuelSDK-Test/StackKeyTest.cs diff --git a/.gitignore b/.gitignore index f12bfd5..5b9d61b 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,4 @@ FuelSDK-CSharp/fixer.awk *.suo FuelSDK-CSharp.v12.suo FuelSDK-Test/App.config +/.vs/ diff --git a/FuelSDK-CSharp/AuthEndpointUriBuilder.cs b/FuelSDK-CSharp/AuthEndpointUriBuilder.cs new file mode 100644 index 0000000..9e27700 --- /dev/null +++ b/FuelSDK-CSharp/AuthEndpointUriBuilder.cs @@ -0,0 +1,32 @@ +using System; + +namespace FuelSDK +{ + public class AuthEndpointUriBuilder + { + private const string legacyQuery = "legacy=1"; + private readonly FuelSDKConfigurationSection configSection; + + public AuthEndpointUriBuilder(FuelSDKConfigurationSection configSection) + { + this.configSection = configSection; + } + + public string Build() + { + UriBuilder uriBuilder = new UriBuilder(configSection.AuthenticationEndPoint); + + if (uriBuilder.Query.ToLower().Contains(legacyQuery)) + { + return uriBuilder.Uri.AbsoluteUri; + } + + if (uriBuilder.Query.Length > 1) + uriBuilder.Query = uriBuilder.Query.Substring(1) + "&" + legacyQuery; + else + uriBuilder.Query = legacyQuery; + + return uriBuilder.Uri.AbsoluteUri; + } + } +} diff --git a/FuelSDK-CSharp/ConfigUtil.cs b/FuelSDK-CSharp/ConfigUtil.cs new file mode 100644 index 0000000..2eec5db --- /dev/null +++ b/FuelSDK-CSharp/ConfigUtil.cs @@ -0,0 +1,12 @@ +using System.Configuration; + +namespace FuelSDK +{ + class ConfigUtil + { + public static FuelSDKConfigurationSection GetFuelSDKConfigSection() + { + return (FuelSDKConfigurationSection)ConfigurationManager.GetSection("fuelSDK"); + } + } +} diff --git a/FuelSDK-CSharp/DefaultEndpoints.cs b/FuelSDK-CSharp/DefaultEndpoints.cs new file mode 100644 index 0000000..efc72f7 --- /dev/null +++ b/FuelSDK-CSharp/DefaultEndpoints.cs @@ -0,0 +1,23 @@ +namespace FuelSDK +{ + /// + /// Contains the default endpoints for the SDK. + /// + public static class DefaultEndpoints + { + /// + /// The default SOAP endpoint + /// + public static string Soap => "https://webservice.s4.exacttarget.com/Service.asmx"; + + /// + /// The default authentication endpoint + /// + public static string Auth => "https://auth.exacttargetapis.com/v1/requestToken?legacy=1"; + + /// + /// The default REST endpoint + /// + public static string Rest => "https://www.exacttargetapis.com"; + } +} diff --git a/FuelSDK-CSharp/ETCampaign.cs b/FuelSDK-CSharp/ETCampaign.cs index 31a7c13..95049cf 100644 --- a/FuelSDK-CSharp/ETCampaign.cs +++ b/FuelSDK-CSharp/ETCampaign.cs @@ -38,7 +38,7 @@ public class ETCampaign : FuelObject /// public ETCampaign() { - Endpoint = "https://www.exacttargetapis.com/hub/v1/campaigns/{ID}"; + Endpoint = ConfigUtil.GetFuelSDKConfigSection().RestEndPoint + "/hub/v1/campaigns/{ID}"; URLProperties = new[] { "ID" }; RequiredURLProperties = new string[0]; } diff --git a/FuelSDK-CSharp/ETCampaignAsset.cs b/FuelSDK-CSharp/ETCampaignAsset.cs index e76745b..d9ee7c2 100644 --- a/FuelSDK-CSharp/ETCampaignAsset.cs +++ b/FuelSDK-CSharp/ETCampaignAsset.cs @@ -33,7 +33,7 @@ public class ETCampaignAsset : FuelObject /// public ETCampaignAsset() { - Endpoint = "https://www.exacttargetapis.com/hub/v1/campaigns/{CampaignID}/assets/{ID}"; + Endpoint = ConfigUtil.GetFuelSDKConfigSection().RestEndPoint + "/hub/v1/campaigns/{CampaignID}/assets/{ID}"; URLProperties = new[] { "CampaignID", "ID" }; RequiredURLProperties = new[] { "CampaignID" }; } diff --git a/FuelSDK-CSharp/ETClient.cs b/FuelSDK-CSharp/ETClient.cs index 2cad0b8..ff4d5d7 100644 --- a/FuelSDK-CSharp/ETClient.cs +++ b/FuelSDK-CSharp/ETClient.cs @@ -5,12 +5,9 @@ using System.IO; using System.Linq; using System.Net; -using System.Reflection; using System.ServiceModel; using System.ServiceModel.Channels; using System.Xml.Linq; -using System.Xml.Serialization; -using System.Xml.XPath; using JWT; using JWT.Serializers; using Newtonsoft.Json.Linq; @@ -23,7 +20,8 @@ namespace FuelSDK /// public class ETClient { - public const string SDKVersion = "FuelSDX-C#-v1.0.0"; + public const string SDKVersion = "FuelSDK-C#-v1.1.0"; + private FuelSDKConfigurationSection configSection; public string AuthToken { get; private set; } public SoapClient SoapClient { get; private set; } @@ -34,7 +32,12 @@ public class ETClient public string EnterpriseId { get; private set; } public string OrganizationId { get; private set; } public string Stack { get; private set; } - private string authEndPoint { get; set; } + + private static DateTime soapEndPointExpiration; + private static DateTime stackKeyExpiration; + private static string fetchedSoapEndpoint; + private const long cacheDurationInMinutes = 10; + public class RefreshState { public string RefreshKey { get; set; } @@ -50,6 +53,9 @@ public ETClient(NameValueCollection parameters = null, RefreshState refreshState // Get configuration file and set variables configSection = (FuelSDKConfigurationSection)ConfigurationManager.GetSection("fuelSDK"); configSection = (configSection != null ? (FuelSDKConfigurationSection)configSection.Clone() : new FuelSDKConfigurationSection()); + configSection = configSection + .WithDefaultAuthEndpoint(DefaultEndpoints.Auth) + .WithDefaultRestEndpoint(DefaultEndpoints.Rest); if (parameters != null) { if (parameters.AllKeys.Contains("appSignature")) @@ -64,6 +70,10 @@ public ETClient(NameValueCollection parameters = null, RefreshState refreshState { configSection.AuthenticationEndPoint = parameters["authEndPoint"]; } + if (parameters.AllKeys.Contains("restEndPoint")) + { + configSection.RestEndPoint = parameters["restEndPoint"]; + } } if (string.IsNullOrEmpty(configSection.ClientId) || string.IsNullOrEmpty(configSection.ClientSecret)) @@ -105,15 +115,9 @@ public ETClient(NameValueCollection parameters = null, RefreshState refreshState organizationFind = true; } - // Find the appropriate endpoint for the acccount - var grSingleEndpoint = new ETEndpoint { AuthStub = this, Type = "soap" }.Get(); - if (grSingleEndpoint.Status && grSingleEndpoint.Results.Length == 1) - configSection.SoapEndPoint = ((ETEndpoint)grSingleEndpoint.Results[0]).URL; - else - throw new Exception("Unable to determine stack using /platform/v1/endpoints: " + grSingleEndpoint.Message); + FetchSoapEndpoint(); // Create the SOAP binding for call with Oauth. - SoapClient = new SoapClient(GetSoapBinding(), new EndpointAddress(new Uri(configSection.SoapEndPoint))); SoapClient.ClientCredentials.UserName.UserName = "*"; SoapClient.ClientCredentials.UserName.Password = "*"; @@ -139,11 +143,44 @@ public ETClient(NameValueCollection parameters = null, RefreshState refreshState { EnterpriseId = results[0].Client.EnterpriseID.ToString(); OrganizationId = results[0].ID.ToString(); - Stack = GetStackFromSoapEndPoint(new Uri(configSection.SoapEndPoint)); + Stack = StackKey.Instance.Get(long.Parse(EnterpriseId), this); } } } + internal string FetchRestAuth() + { + var returnedRestAuthEndpoint = new ETEndpoint { AuthStub = this, Type = "restAuth" }.Get(); + if (returnedRestAuthEndpoint.Status && returnedRestAuthEndpoint.Results.Length == 1) + return ((ETEndpoint)returnedRestAuthEndpoint.Results[0]).URL; + else + throw new Exception("REST auth endpoint could not be determined"); + } + + private void FetchSoapEndpoint() + { + if (string.IsNullOrEmpty(configSection.SoapEndPoint) || (DateTime.Now > soapEndPointExpiration && fetchedSoapEndpoint != null)) + { + try + { + var grSingleEndpoint = new ETEndpoint { AuthStub = this, Type = "soap" }.Get(); + if (grSingleEndpoint.Status && grSingleEndpoint.Results.Length == 1) + { + // Find the appropriate endpoint for the account + configSection.SoapEndPoint = ((ETEndpoint)grSingleEndpoint.Results[0]).URL; + fetchedSoapEndpoint = configSection.SoapEndPoint; + soapEndPointExpiration = DateTime.Now.AddMinutes(cacheDurationInMinutes); + } + else + configSection.SoapEndPoint = DefaultEndpoints.Soap; + } + catch + { + configSection.SoapEndPoint = DefaultEndpoints.Soap; + } + } + } + private string DecodeJWT(string jwt, string key) { IJsonSerializer serializer = new JsonNetSerializer(); @@ -156,14 +193,6 @@ private string DecodeJWT(string jwt, string key) return json; } - private string GetStackFromSoapEndPoint(Uri uri) - { - var parts = uri.Host.Split('.'); - if (parts.Length < 2 || !parts[0].Equals("webservice", StringComparison.OrdinalIgnoreCase)) - throw new Exception("not exacttarget.com"); - return (parts[1] == "exacttarget" ? "s1" : parts[1].ToLower()); - } - private static Binding GetSoapBinding() { return new CustomBinding(new BindingElementCollection @@ -200,15 +229,17 @@ private static Binding GetSoapBinding() public void RefreshToken(bool force = false) { + // workaround to support TLS 1.2 in .NET 4.0 (source: https://blogs.perficient.com/2016/04/28/tsl-1-2-and-net-support/) + ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; // RefreshToken if (!string.IsNullOrEmpty(AuthToken) && DateTime.Now.AddSeconds(300) <= AuthTokenExpiration && !force) return; // Get an internalAuthToken using clientId and clientSecret - var strURL = configSection.AuthenticationEndPoint; + var authEndpoint = new AuthEndpointUriBuilder(configSection).Build(); // Build the request - var request = (HttpWebRequest)WebRequest.Create(strURL.Trim()); + var request = (HttpWebRequest)WebRequest.Create(authEndpoint.Trim()); request.Method = "POST"; request.ContentType = "application/json"; request.UserAgent = SDKVersion; diff --git a/FuelSDK-CSharp/ETEndpoint.cs b/FuelSDK-CSharp/ETEndpoint.cs index 2b2e6d1..88d8f82 100644 --- a/FuelSDK-CSharp/ETEndpoint.cs +++ b/FuelSDK-CSharp/ETEndpoint.cs @@ -1,4 +1,5 @@ using System; +using System.Configuration; using Newtonsoft.Json.Linq; namespace FuelSDK @@ -23,7 +24,7 @@ public class ETEndpoint : FuelObject /// public ETEndpoint() { - Endpoint = "https://www.exacttargetapis.com/platform/v1/endpoints/{Type}"; + Endpoint = ConfigUtil.GetFuelSDKConfigSection().RestEndPoint + "/platform/v1/endpoints/{Type}"; URLProperties = new[] { "Type" }; RequiredURLProperties = new string[0]; } diff --git a/FuelSDK-CSharp/FuelReturn.cs b/FuelSDK-CSharp/FuelReturn.cs index b18ac20..cc6ea11 100644 --- a/FuelSDK-CSharp/FuelReturn.cs +++ b/FuelSDK-CSharp/FuelReturn.cs @@ -410,11 +410,11 @@ protected TResult[] ExecuteAPI(Func select client.RefreshToken(); using (var scope = new OperationContextScope(client.SoapClient.InnerChannel)) { - // Add oAuth token to SOAP header. - XNamespace ns = "http://exacttarget.com"; - var oauthElement = new XElement(ns + "oAuthToken", client.InternalAuthToken); - var xmlHeader = MessageHeader.CreateHeader("oAuth", "http://exacttarget.com", oauthElement); - OperationContext.Current.OutgoingMessageHeaders.Add(xmlHeader); + // Add oAuth token to SOAP header. + XNamespace ns = "http://exacttarget.com"; + var oauthElement = new XElement(ns + "oAuthToken", client.InternalAuthToken); + var xmlHeader = MessageHeader.CreateHeader("oAuth", "http://exacttarget.com", oauthElement); + OperationContext.Current.OutgoingMessageHeaders.Add(xmlHeader); var httpRequest = new System.ServiceModel.Channels.HttpRequestMessageProperty(); OperationContext.Current.OutgoingMessageProperties.Add(System.ServiceModel.Channels.HttpRequestMessageProperty.Name, httpRequest); @@ -427,16 +427,6 @@ protected TResult[] ExecuteAPI(Func select MoreResults = (response.OverallStatus == "MoreDataAvailable"); Message = (response.OverallStatusMessage ?? string.Empty); - string r; - APIObject[] a; - var d = client.SoapClient.Retrieve( - new RetrieveRequest - { - ObjectType = "BusinessUnit", - Properties = new[] { "ID", "Name" } - }, out r, out a - ); - return response.Results; } } @@ -479,12 +469,12 @@ protected string ExecuteFuel(FuelObject obj, string[] required, string method, b foreach (string urlProp in obj.URLProperties) completeURL = completeURL.Replace("{" + urlProp + "}", string.Empty); - completeURL += "?access_token=" + obj.AuthStub.AuthToken; - if (obj.Page != 0) - completeURL += "&page=" + obj.Page.ToString(); + if (obj.Page.HasValue && obj.Page.Value > 0) + completeURL += "?page=" + obj.Page.ToString(); - var request = (HttpWebRequest)WebRequest.Create(completeURL.Trim()); - request.Method = method; + var request = (HttpWebRequest)WebRequest.Create(completeURL.Trim()); + request.Headers.Add("Authorization", "Bearer " + obj.AuthStub.AuthToken); + request.Method = method; request.ContentType = "application/json"; request.UserAgent = ETClient.SDKVersion; diff --git a/FuelSDK-CSharp/FuelSDK-CSharp.csproj b/FuelSDK-CSharp/FuelSDK-CSharp.csproj index edf8e8c..970351b 100644 --- a/FuelSDK-CSharp/FuelSDK-CSharp.csproj +++ b/FuelSDK-CSharp/FuelSDK-CSharp.csproj @@ -78,6 +78,9 @@ + + + @@ -132,6 +135,8 @@ + + diff --git a/FuelSDK-CSharp/FuelSDKConfigurationSection.cs b/FuelSDK-CSharp/FuelSDKConfigurationSection.cs index 5a2a6e8..2467932 100644 --- a/FuelSDK-CSharp/FuelSDKConfigurationSection.cs +++ b/FuelSDK-CSharp/FuelSDKConfigurationSection.cs @@ -42,19 +42,33 @@ public string ClientSecret /// Gets or sets the SOAP end point. /// /// The SOAP end point. - [ConfigurationProperty("soapEndPoint", DefaultValue = "https://webservice.s4.exacttarget.com/Service.asmx")] + [ConfigurationProperty("soapEndPoint")] public string SoapEndPoint { get { return (string)this["soapEndPoint"]; } set { this["soapEndPoint"] = value; } } - [ConfigurationProperty("authEndPoint", DefaultValue = "https://auth-qa.exacttargetapis.com/v1/requestToken?legacy=1")] + /// + /// Gets or sets the authentification end point. + /// + /// The authentification end point. + [ConfigurationProperty("authEndPoint")] public string AuthenticationEndPoint { get { return (string)this["authEndPoint"]; } set { this["authEndPoint"] = value; } } /// + /// Gets or sets the REST end point. + /// + /// The REST end point. + [ConfigurationProperty("restEndPoint")] + public string RestEndPoint + { + get { return (string)this["restEndPoint"]; } + set { this["restEndPoint"] = value; } + } + /// /// Clone this instance. /// /// The clone. @@ -70,5 +84,30 @@ public override bool IsReadOnly() { return false; } + + /// + /// Sets the AuthenticationEndPoint to the default value if it is not set and returns the updated instance. + /// + /// The default auth endpoint + /// The updated instance + public FuelSDKConfigurationSection WithDefaultAuthEndpoint(string defaultAuthEndpoint) + { + if (string.IsNullOrEmpty(AuthenticationEndPoint)) + { + AuthenticationEndPoint = defaultAuthEndpoint; + } + + return this; + } + + public FuelSDKConfigurationSection WithDefaultRestEndpoint(string defaultRestEndpoint) + { + if (string.IsNullOrEmpty(RestEndPoint)) + { + this.RestEndPoint = defaultRestEndpoint; + } + + return this; + } } } diff --git a/FuelSDK-CSharp/StackKey.cs b/FuelSDK-CSharp/StackKey.cs new file mode 100644 index 0000000..7232a33 --- /dev/null +++ b/FuelSDK-CSharp/StackKey.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Concurrent; + +namespace FuelSDK +{ + public class StackKey + { + ConcurrentDictionary values; + + private static readonly Lazy lazy = + new Lazy(() => new StackKey()); + + public static StackKey Instance + { + get + { + return lazy.Value; + } + } + + private StackKey() + { + values = new ConcurrentDictionary(); + } + + public string Get(long enterpriseId, ETClient client) + { + return values.GetOrAdd(enterpriseId, (eId) => { + var restAuth = client.FetchRestAuth(); + var userInfo = new UserInfo(restAuth) + { + AuthStub = client + }; + return ((UserInfo)userInfo.Get().Results[0]).StackKey; + }); + } + } +} diff --git a/FuelSDK-CSharp/UserInfo.cs b/FuelSDK-CSharp/UserInfo.cs new file mode 100644 index 0000000..f51ee76 --- /dev/null +++ b/FuelSDK-CSharp/UserInfo.cs @@ -0,0 +1,39 @@ +using Newtonsoft.Json.Linq; + +namespace FuelSDK +{ + public class UserInfo : FuelObject + { + public string StackKey { get; set; } + /// + /// Initializes a new instance of the class. + /// + /// The auth rest endpoint. + public UserInfo(string authRestEndpoint) + { + Endpoint = authRestEndpoint + "/v2/userinfo"; + URLProperties = new string[0]; + RequiredURLProperties = new string[0]; + } + /// + /// Initializes a new instance of the class. + /// + /// Javascript Object. + public UserInfo(JObject obj) + { + if (obj["organization"]["stack_key"] != null) + { + StackKey = obj["organization"]["stack_key"].ToString(); + } + } + /// Get this instance. + /// + /// The object.. + public GetReturn Get() { var r = new GetReturn(this); Page = r.LastPageNumber; return r; } + /// + /// Gets the more results. + /// + /// The object.. + public GetReturn GetMoreResults() { Page++; var r = new GetReturn(this); Page = r.LastPageNumber; return r; } + } +} \ No newline at end of file diff --git a/FuelSDK-Test/AuthEndpointUriBuilderTest.cs b/FuelSDK-Test/AuthEndpointUriBuilderTest.cs new file mode 100644 index 0000000..c27ac07 --- /dev/null +++ b/FuelSDK-Test/AuthEndpointUriBuilderTest.cs @@ -0,0 +1,52 @@ +using NUnit.Framework; + +namespace FuelSDK.Test +{ + [TestFixture] + public class AuthEndpointUriBuilderTest : CustomConfigSectionBasedTest + { + [Test] + public void BuilderAddsLegacyQueryParamWhenNoParamsArePresent() + { + FuelSDKConfigurationSection configSection = GetCustomConfigurationSectionFromConfigFile(authEndpointMissingLegacyQueryParamFileName); + + AuthEndpointUriBuilder builder = new AuthEndpointUriBuilder(configSection); + var uri = builder.Build(); + + Assert.AreEqual("https://authendpoint.com/v1/requestToken?legacy=1", uri); + } + + [Test] + public void BuilderReturnsCorrectAuthEndpointUriWhenLegacyQueryIsPresent() + { + FuelSDKConfigurationSection configSection = GetCustomConfigurationSectionFromConfigFile(authEndpointWithLegacyQueryParamFileName); + + AuthEndpointUriBuilder builder = new AuthEndpointUriBuilder(configSection); + var uri = builder.Build(); + + Assert.AreEqual("https://authendpoint.com/v1/requestToken?legacy=1", uri); + } + + [Test] + public void BuilderAddsLegacyQueryParamWhenOtherParamsArePresent() + { + FuelSDKConfigurationSection configSection = GetCustomConfigurationSectionFromConfigFile(authEndpointWithMultipleQueryParamsButMissingLegacyParamFileName); + + AuthEndpointUriBuilder builder = new AuthEndpointUriBuilder(configSection); + var uri = builder.Build(); + + Assert.AreEqual("https://authendpoint.com/v1/requestToken?param1=1&legacy=1", uri); + } + + [Test] + public void BuilderReturnsCorrectAuthEndpointUriWhenLegacyQueryIsPresentAlongwithOtherQueryParams() + { + FuelSDKConfigurationSection configSection = GetCustomConfigurationSectionFromConfigFile(authEndpointWithMultipleQueryParamsIncludingLegacyParamFileName); + + AuthEndpointUriBuilder builder = new AuthEndpointUriBuilder(configSection); + var uri = builder.Build(); + + Assert.AreEqual("https://authendpoint.com/v1/requestToken?param1=1&legacy=1", uri); + } + } +} diff --git a/FuelSDK-Test/ConfigFiles/allPropertiesSet.config b/FuelSDK-Test/ConfigFiles/allPropertiesSet.config new file mode 100644 index 0000000..72ec119 --- /dev/null +++ b/FuelSDK-Test/ConfigFiles/allPropertiesSet.config @@ -0,0 +1,7 @@ + + + +
+ + + \ No newline at end of file diff --git a/FuelSDK-Test/ConfigFiles/allPropertiesSetButAuthEndpointIsEmpty.config b/FuelSDK-Test/ConfigFiles/allPropertiesSetButAuthEndpointIsEmpty.config new file mode 100644 index 0000000..0444038 --- /dev/null +++ b/FuelSDK-Test/ConfigFiles/allPropertiesSetButAuthEndpointIsEmpty.config @@ -0,0 +1,7 @@ + + + +
+ + + \ No newline at end of file diff --git a/FuelSDK-Test/ConfigFiles/allPropertiesSetButRestEndpointIsEmpty.config b/FuelSDK-Test/ConfigFiles/allPropertiesSetButRestEndpointIsEmpty.config new file mode 100644 index 0000000..20841cf --- /dev/null +++ b/FuelSDK-Test/ConfigFiles/allPropertiesSetButRestEndpointIsEmpty.config @@ -0,0 +1,7 @@ + + + +
+ + + \ No newline at end of file diff --git a/FuelSDK-Test/ConfigFiles/authEndpointMissingLegacyQueryParam.config b/FuelSDK-Test/ConfigFiles/authEndpointMissingLegacyQueryParam.config new file mode 100644 index 0000000..68ced19 --- /dev/null +++ b/FuelSDK-Test/ConfigFiles/authEndpointMissingLegacyQueryParam.config @@ -0,0 +1,7 @@ + + + +
+ + + \ No newline at end of file diff --git a/FuelSDK-Test/ConfigFiles/authEndpointWithLegacyQueryParam.config b/FuelSDK-Test/ConfigFiles/authEndpointWithLegacyQueryParam.config new file mode 100644 index 0000000..8053628 --- /dev/null +++ b/FuelSDK-Test/ConfigFiles/authEndpointWithLegacyQueryParam.config @@ -0,0 +1,7 @@ + + + +
+ + + \ No newline at end of file diff --git a/FuelSDK-Test/ConfigFiles/authEndpointWithMultipleQueryParamsButMissingLegacyParam.config b/FuelSDK-Test/ConfigFiles/authEndpointWithMultipleQueryParamsButMissingLegacyParam.config new file mode 100644 index 0000000..6e7d3d2 --- /dev/null +++ b/FuelSDK-Test/ConfigFiles/authEndpointWithMultipleQueryParamsButMissingLegacyParam.config @@ -0,0 +1,7 @@ + + + +
+ + + \ No newline at end of file diff --git a/FuelSDK-Test/ConfigFiles/authEndpointWithMultipleQueryParamsIncludingLegacyParam.config b/FuelSDK-Test/ConfigFiles/authEndpointWithMultipleQueryParamsIncludingLegacyParam.config new file mode 100644 index 0000000..10c14a1 --- /dev/null +++ b/FuelSDK-Test/ConfigFiles/authEndpointWithMultipleQueryParamsIncludingLegacyParam.config @@ -0,0 +1,7 @@ + + + +
+ + + \ No newline at end of file diff --git a/FuelSDK-Test/ConfigFiles/empty.config b/FuelSDK-Test/ConfigFiles/empty.config new file mode 100644 index 0000000..49cc43e --- /dev/null +++ b/FuelSDK-Test/ConfigFiles/empty.config @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/FuelSDK-Test/ConfigFiles/missingRequiredAppSignatureProperty.config b/FuelSDK-Test/ConfigFiles/missingRequiredAppSignatureProperty.config new file mode 100644 index 0000000..7e3c264 --- /dev/null +++ b/FuelSDK-Test/ConfigFiles/missingRequiredAppSignatureProperty.config @@ -0,0 +1,7 @@ + + + +
+ + + \ No newline at end of file diff --git a/FuelSDK-Test/ConfigFiles/missingRequiredClientIdProperty.config b/FuelSDK-Test/ConfigFiles/missingRequiredClientIdProperty.config new file mode 100644 index 0000000..8545841 --- /dev/null +++ b/FuelSDK-Test/ConfigFiles/missingRequiredClientIdProperty.config @@ -0,0 +1,7 @@ + + + +
+ + + \ No newline at end of file diff --git a/FuelSDK-Test/ConfigFiles/missingRequiredClientSecretProperty.config b/FuelSDK-Test/ConfigFiles/missingRequiredClientSecretProperty.config new file mode 100644 index 0000000..f9fe1e4 --- /dev/null +++ b/FuelSDK-Test/ConfigFiles/missingRequiredClientSecretProperty.config @@ -0,0 +1,7 @@ + + + +
+ + + \ No newline at end of file diff --git a/FuelSDK-Test/ConfigFiles/requiredPropertiesOnly.config b/FuelSDK-Test/ConfigFiles/requiredPropertiesOnly.config new file mode 100644 index 0000000..9bc6136 --- /dev/null +++ b/FuelSDK-Test/ConfigFiles/requiredPropertiesOnly.config @@ -0,0 +1,7 @@ + + + +
+ + + \ No newline at end of file diff --git a/FuelSDK-Test/CustomConfigSectionBasedTest.cs b/FuelSDK-Test/CustomConfigSectionBasedTest.cs new file mode 100644 index 0000000..0736ac0 --- /dev/null +++ b/FuelSDK-Test/CustomConfigSectionBasedTest.cs @@ -0,0 +1,34 @@ +using System.Configuration; +using System.IO; +using System.Reflection; + +namespace FuelSDK.Test +{ + public class CustomConfigSectionBasedTest + { + protected readonly string emptyConfigFileName = "empty.config"; + protected readonly string missingRequiredAppSignaturePropertyConfigFileName = "missingRequiredAppSignatureProperty.config"; + protected readonly string missingRequiredClientIdConfigFileName = "missingRequiredClientIdProperty.config"; + protected readonly string missingRequiredClientSecretConfigFileName = "missingRequiredClientSecretProperty.config"; + protected readonly string requiredPropertiesOnlyConfigFileName = "requiredPropertiesOnly.config"; + protected readonly string allPropertiesSetConfigFileName = "allPropertiesSet.config"; + protected readonly string allPropertiesSetButAuthEndpointIsEmptyConfigFileName = "allPropertiesSetButAuthEndpointIsEmpty.config"; + protected readonly string allPropertiesSetButRestEndpointIsEmptyConfigFileName = "allPropertiesSetButRestEndpointIsEmpty.config"; + protected readonly string authEndpointMissingLegacyQueryParamFileName = "authEndpointMissingLegacyQueryParam.config"; + protected readonly string authEndpointWithLegacyQueryParamFileName = "authEndpointWithLegacyQueryParam.config"; + protected readonly string authEndpointWithMultipleQueryParamsButMissingLegacyParamFileName = "authEndpointWithMultipleQueryParamsButMissingLegacyParam.config"; + protected readonly string authEndpointWithMultipleQueryParamsIncludingLegacyParamFileName = "authEndpointWithMultipleQueryParamsButMissingLegacyParam.config"; + + protected FuelSDKConfigurationSection GetCustomConfigurationSectionFromConfigFile(string configFileName) + { + ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap(); + fileMap.ExeConfigFilename = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "ConfigFiles", configFileName); + + Configuration config + = ConfigurationManager.OpenMappedExeConfiguration(fileMap, + ConfigurationUserLevel.None); + + return config.GetSection("fuelSDK") as FuelSDKConfigurationSection; + } + } +} diff --git a/FuelSDK-Test/ETClientTest.cs b/FuelSDK-Test/ETClientTest.cs new file mode 100644 index 0000000..9703d94 --- /dev/null +++ b/FuelSDK-Test/ETClientTest.cs @@ -0,0 +1,42 @@ +using NUnit.Framework; +using System; +using System.Reflection; + +namespace FuelSDK.Test +{ + [TestFixture()] + class ETClientTest + { + ETClient client1; + ETClient client2; + + [OneTimeSetUp] + public void Setup() + { + client1 = new ETClient(); + client2 = new ETClient(); + } + + [Test()] + public void GetClientStack() + { + Assert.IsNotNull(client1.Stack); + Assert.IsNotNull(client2.Stack); + Assert.AreEqual(client1.Stack, client2.Stack); + } + + [Test()] + public void TestSoapEndpointCaching() + { + var client1SoapEndpointExpirationField = client1.GetType().GetField("soapEndPointExpiration", BindingFlags.NonPublic | BindingFlags.Static); + var client2SoapEndpointExpirationField = client2.GetType().GetField("soapEndPointExpiration", BindingFlags.NonPublic | BindingFlags.Static); + + var client1SoapEndpointExpiration = (DateTime)client1SoapEndpointExpirationField.GetValue(null); + var client2SoapEndpointExpiration = (DateTime)client2SoapEndpointExpirationField.GetValue(null); + + Assert.IsTrue(client1SoapEndpointExpiration > DateTime.MinValue); + Assert.IsTrue(client2SoapEndpointExpiration > DateTime.MinValue); + Assert.AreEqual(client1SoapEndpointExpiration, client2SoapEndpointExpiration); + } + } +} diff --git a/FuelSDK-Test/FuelSDK.Test.csproj b/FuelSDK-Test/FuelSDK.Test.csproj index f820180..ccb01cd 100644 --- a/FuelSDK-Test/FuelSDK.Test.csproj +++ b/FuelSDK-Test/FuelSDK.Test.csproj @@ -32,10 +32,19 @@ ..\packages\NUnit.3.7.1\lib\net40\nunit.framework.dll True + + ..\packages\NUnit3TestAdapter.3.7.0\tools\NUnit3.TestAdapter.dll + True + + + + + + @@ -51,10 +60,49 @@ + + PreserveNewest + Designer + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always diff --git a/FuelSDK-Test/FuelSDKConfigurationSectionTest.cs b/FuelSDK-Test/FuelSDKConfigurationSectionTest.cs new file mode 100644 index 0000000..7b28fd5 --- /dev/null +++ b/FuelSDK-Test/FuelSDKConfigurationSectionTest.cs @@ -0,0 +1,112 @@ +using NUnit.Framework; +using System.Configuration; + +namespace FuelSDK.Test +{ + [TestFixture] + class FuelSDKConfigurationSectionTest : CustomConfigSectionBasedTest + { + [Test()] + public void NoCustomConfigSection() + { + FuelSDKConfigurationSection section = GetCustomConfigurationSectionFromConfigFile(emptyConfigFileName); + Assert.IsNull(section); + } + + [Test()] + public void MissingRequiredAppSignaturePropertyFromConfigSection() + { + Assert.That(() => GetCustomConfigurationSectionFromConfigFile(missingRequiredAppSignaturePropertyConfigFileName), Throws.TypeOf()); + } + + [Test()] + public void MissingRequiredClientIdPropertyFromConfigSection() + { + Assert.That(() => GetCustomConfigurationSectionFromConfigFile(missingRequiredClientIdConfigFileName), Throws.TypeOf()); + } + + [Test()] + public void MissingRequiredClientSecretPropertyFromConfigSection() + { + Assert.That(() => GetCustomConfigurationSectionFromConfigFile(missingRequiredClientSecretConfigFileName), Throws.TypeOf()); + } + + [Test()] + public void MissingSoapEndPointPropertyFromConfigSection() + { + FuelSDKConfigurationSection section = GetCustomConfigurationSectionFromConfigFile(requiredPropertiesOnlyConfigFileName); + Assert.AreEqual(string.Empty, section.SoapEndPoint); + } + + [Test()] + public void MissingAuthEndPointPropertyFromConfigSection() + { + FuelSDKConfigurationSection section = GetCustomConfigurationSectionFromConfigFile(requiredPropertiesOnlyConfigFileName); + Assert.AreEqual(string.Empty, section.AuthenticationEndPoint); + } + + [Test()] + public void MissingRestEndPointPropertyFromConfigSection() + { + FuelSDKConfigurationSection section = GetCustomConfigurationSectionFromConfigFile(requiredPropertiesOnlyConfigFileName); + Assert.AreEqual(string.Empty, section.RestEndPoint); + } + + [Test()] + public void AllPropertiesSetInConfigSection() + { + FuelSDKConfigurationSection section = GetCustomConfigurationSectionFromConfigFile(allPropertiesSetConfigFileName); + Assert.AreEqual(section.AppSignature, "none"); + Assert.AreEqual(section.ClientId, "abc"); + Assert.AreEqual(section.ClientSecret, "cde"); + Assert.AreEqual(section.SoapEndPoint, "https://soapendpoint.com"); + Assert.AreEqual(section.AuthenticationEndPoint, "https://authendpoint.com"); + Assert.AreEqual(section.RestEndPoint, "https://restendpoint.com"); + } + + [Test] + public void AllPropertiesSetButAuthEndpointIsEmpty() + { + FuelSDKConfigurationSection section = GetCustomConfigurationSectionFromConfigFile(allPropertiesSetButAuthEndpointIsEmptyConfigFileName); + var sectionWithDefaultAuthEndpoint = section.WithDefaultAuthEndpoint(DefaultEndpoints.Auth); + + Assert.AreEqual(DefaultEndpoints.Auth, sectionWithDefaultAuthEndpoint.AuthenticationEndPoint); + } + + [Test] + public void AllPropertiesSetButRestEndpointIsEmpty() + { + FuelSDKConfigurationSection section = GetCustomConfigurationSectionFromConfigFile(allPropertiesSetButRestEndpointIsEmptyConfigFileName); + var sectionWithDefaultRestEndpoint = section.WithDefaultRestEndpoint(DefaultEndpoints.Rest); + + Assert.AreEqual(DefaultEndpoints.Rest, sectionWithDefaultRestEndpoint.RestEndPoint); + } + + [Test] + public void WithDefaultsDoesNotOverwriteValuesSetInConfig() + { + FuelSDKConfigurationSection section = GetCustomConfigurationSectionFromConfigFile(allPropertiesSetConfigFileName); + section = section + .WithDefaultRestEndpoint(DefaultEndpoints.Rest) + .WithDefaultAuthEndpoint(DefaultEndpoints.Auth); + + Assert.AreEqual(section.AuthenticationEndPoint, "https://authendpoint.com"); + Assert.AreEqual(section.RestEndPoint, "https://restendpoint.com"); + } + + [Test] + public void ModifyingAClonedConfigSectionAffectsTheOriginalSectionAndAnyNewInstance() + { + var section = (FuelSDKConfigurationSection)ConfigurationManager.GetSection("fuelSDK"); + var clonedSection = (FuelSDKConfigurationSection)section.Clone(); + + clonedSection.SoapEndPoint = "https://soapendpoint.com"; + var newSection = (FuelSDKConfigurationSection)ConfigurationManager.GetSection("fuelSDK"); + + Assert.AreEqual(object.ReferenceEquals(section, clonedSection), false); + Assert.AreNotSame(section, clonedSection); + Assert.AreEqual(section.SoapEndPoint, clonedSection.SoapEndPoint); + Assert.AreEqual(section.SoapEndPoint, newSection.SoapEndPoint); + } + } +} diff --git a/FuelSDK-Test/StackKeyTest.cs b/FuelSDK-Test/StackKeyTest.cs new file mode 100644 index 0000000..c39aa40 --- /dev/null +++ b/FuelSDK-Test/StackKeyTest.cs @@ -0,0 +1,19 @@ +using NUnit.Framework; + +namespace FuelSDK.Test +{ + [TestFixture] + public class StackKeyTest + { + [Test] + public void MultipleETClientInstancesForTheSameClientIdAndSecretWillHaveTheSameStackKey() + { + ETClient client1 = new ETClient(); + ETClient client2 = new ETClient(); + + Assert.IsNotNull(client1.Stack); + Assert.IsNotNull(client2.Stack); + Assert.AreEqual(client1.Stack, client2.Stack); + } + } +} diff --git a/README.md b/README.md index a306322..b7c0729 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,26 @@ Salesforce Marketing Cloud Fuel SDK for C# ## Overview ## The Fuel SDK for C# provides easy access to Salesforce Marketing Cloud's Fuel API Family services, including a collection of REST APIs and a SOAP API. These APIs provide access to Salesforce Marketing Cloud functionality via common collection types. +## New Features in Version 1.1.0 ## +* **Added support for your tenant's endpoints - [More Details](https://developer.salesforce.com/docs/atlas.en-us.mc-apis.meta/mc-apis/your-subdomain-tenant-specific-endpoints.htm) :** The user of the SDK can now configure them through a **App.config** file OR using the “**parameters**” **ETClient** constructor parameter as in the previous version of the SDK. The user of the SDK has to make a copy of the **App.config.transform** file which is found in the **FuelSDK-CSharp** folder, place it in the same folder and rename it to **App.config**. The structure of this file will be the following: + +
 
+<configuration>
+  <configSections>
+    <section name="fuelSDK" type="FuelSDK.FuelSDKConfigurationSection, FuelSDK" />
+  </configSections>
+  <fuelSDK 
+    appSignature="<appSignature>" 
+    clientId="<clientId>" 
+    clientSecret="<clientSecret>" 
+    authEndPoint="<authenticationEndPoint>" 
+    soapEndPoint="<soapEndPoint>" 
+    restEndPoint="<restEndPoint>" />
+</configuration>
+
+
+ + ## New Features in Version 1.0.0 ## * **code refactor :** code refactored to individual class files. Classes starting with “ET_” are deprecated now and all SDK API objects start with “ET”. @@ -21,6 +41,12 @@ Project tree structure * **JWT :** JWT.cs is removed from the project and added as dependency. +Not specifying the **appSignature** field in the **App.config** file will throw an error if you try to instantiate the **ETClient** class using a **jwt**. + +Not specifying the **authEndPoint**, **soapEndPoint** and the **restEndPoint** in the **App.config** file will make the SDK use the default values of these fields that are provided in the **FuelSDKConfigurationSection** class. + +Not specifying the **clientId** and the **clientSecret** fields in the **App.config** file will throw an error if you try to instantiate the **ETClient** class. + ## Requirements ## - .NET Studio 2013 or higher (WCF) - .NET Framework 4 @@ -39,7 +65,7 @@ All necessary settings are in App.config.transform file. ## Getting Started ## The FuelSDK-CSharp solution file includes two projects. One for the actual SDK and one for a web based testing app as an example of how to use the SDK dll and other dependent files. -Rename the FuelSDK_config.xml.template file in the objsamples project to FuelSDK_config.xml, then edit so you can input the ClientID and Client Secret values provided when you registered your application. If you are building a HubExchange application for the Interactive Marketing Hub then, you must also provide the Application Signature (appsignature). Only change the value for the defaultwsdl configuration item if instructed by Salesforce Marketing Cloud. +Rename the **App.config.transform** file in the objsamples project to **App.config**, then edit so you can input the ClientID and Client Secret values provided when you registered your application. If you are building a HubExchange application for the Interactive Marketing Hub then, you must also provide the Application Signature (appsignature). Only change the value for the defaultwsdl configuration item if instructed by Salesforce Marketing Cloud. If you have not registered your application or you need to lookup your Application Key or Application Signature values, please go to App Center at [Code@: Salesforce Marketing Cloud's Developer Community]( https://appcenter-auth.s1.marketingcloudapps.com "CODE@"). @@ -131,7 +157,7 @@ Get Methods also return an addition value to indicate if more information is ava The objsamples project (included in solution) contains sample calls for all the available functionality. ## Copyright and license ## -Copyright (c) 2017 Salesforce Marketing Cloud +Copyright (c) 2018 Salesforce Marketing Cloud Licensed under the MIT License (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the COPYING file. diff --git a/nuspecs/FuelSDK-CSharp.nuspec b/nuspecs/FuelSDK-CSharp.nuspec index a277a54..d56de88 100644 --- a/nuspecs/FuelSDK-CSharp.nuspec +++ b/nuspecs/FuelSDK-CSharp.nuspec @@ -2,7 +2,7 @@ SFMC.FuelSDK - 1.0.0 + 1.1.0 FuelSDK-CSharp Salesforce Salesforce @@ -11,7 +11,7 @@ false The Fuel SDK for C# provides easy access to Salesforce Marketing Cloud's (ExactTarge) Fuel API Family services, including a collection of REST APIs and a SOAP API. These APIs provide access to Salesforce Marketing Cloud functionality via common collection types. - Copyright 2017 + Copyright 2018 FuelSdk Saleforce Marketing Cloud ExactTarget en-US