diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs index 40aa3945..133548dc 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs @@ -1,5 +1,6 @@ using System; using System.Data.Common; +using System.IO; using System.Net; using System.Net.Http; using System.Text; @@ -10,6 +11,7 @@ using JSONAPI.Json; using Microsoft.Owin.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Owin; namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests { @@ -20,19 +22,18 @@ public abstract class AcceptanceTestsBase private static readonly Regex GuidRegex = new Regex(@"\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b", RegexOptions.IgnoreCase); //private static readonly Regex StackTraceRegex = new Regex(@"""stackTrace"":[\s]*""[\w\:\\\.\s\,\-]*"""); private static readonly Regex StackTraceRegex = new Regex(@"""stackTrace""[\s]*:[\s]*"".*?"""); - private static readonly Uri BaseUri = new Uri("https://www.example.com"); + protected static Uri BaseUri = new Uri("https://www.example.com"); protected static DbConnection GetEffortConnection() { return TestHelpers.GetEffortConnection(@"Data"); } - protected static async Task AssertResponseContent(HttpResponseMessage response, string expectedResponseTextResourcePath, HttpStatusCode expectedStatusCode, bool redactErrorData = false) + protected virtual async Task AssertResponseContent(HttpResponseMessage response, string expectedResponseTextResourcePath, HttpStatusCode expectedStatusCode, bool redactErrorData = false) { var responseContent = await response.Content.ReadAsStringAsync(); - var expectedResponse = - JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); + var expectedResponse = ExpectedResponse(expectedResponseTextResourcePath); string actualResponse; if (redactErrorData) { @@ -51,6 +52,13 @@ protected static async Task AssertResponseContent(HttpResponseMessage response, response.StatusCode.Should().Be(expectedStatusCode); } + protected virtual string ExpectedResponse(string expectedResponseTextResourcePath) + { + var expectedResponse = + JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); + return expectedResponse; + } + #region GET protected async Task SubmitGet(DbConnection effortConnection, string requestPath) @@ -58,7 +66,7 @@ protected async Task SubmitGet(DbConnection effortConnectio using (var server = TestServer.Create(app => { var startup = new Startup(() => new TestDbContext(effortConnection, false)); - startup.Configuration(app); + StartupConfiguration(startup, app); })) { var uri = new Uri(BaseUri, requestPath); @@ -75,7 +83,7 @@ protected async Task SubmitPost(DbConnection effortConnecti using (var server = TestServer.Create(app => { var startup = new Startup(() => new TestDbContext(effortConnection, false)); - startup.Configuration(app); + StartupConfiguration(startup, app); })) { var uri = new Uri(BaseUri, requestPath); @@ -100,7 +108,7 @@ protected async Task SubmitPatch(DbConnection effortConnect using (var server = TestServer.Create(app => { var startup = new Startup(() => new TestDbContext(effortConnection, false)); - startup.Configuration(app); + StartupConfiguration(startup, app); })) { var uri = new Uri(BaseUri, requestPath); @@ -124,7 +132,7 @@ protected async Task SubmitDelete(DbConnection effortConnec using (var server = TestServer.Create(app => { var startup = new Startup(() => new TestDbContext(effortConnection, false)); - startup.Configuration(app); + StartupConfiguration(startup, app); })) { var uri = new Uri(BaseUri, requestPath); @@ -137,5 +145,22 @@ protected async Task SubmitDelete(DbConnection effortConnec } #endregion + + + + #region configure startup + + /// + /// Startup process was divided into 4 steps to support better acceptance tests. + /// This method can be overridden by subclass to change behavior of setup. + /// + /// + /// + protected virtual void StartupConfiguration(Startup startup, IAppBuilder app) + { + startup.Configuration(app); + } + + #endregion } } diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/BaseUrlTest.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/BaseUrlTest.cs new file mode 100644 index 00000000..cda8ccf8 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/BaseUrlTest.cs @@ -0,0 +1,166 @@ +using System; +using System.Data.SqlTypes; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using JSONAPI.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Owin; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class BaseUrlTest : AcceptanceTestsBase + { + [TestInitialize] + public void TestInit() + { + if (!BaseUri.AbsoluteUri.EndsWith("api/")) + { + BaseUri = new Uri(BaseUri.AbsoluteUri + "api/"); + } + } + [TestCleanup] + public void TestCleanup() + { + if (BaseUri.AbsoluteUri.EndsWith("api/")) + { + BaseUri = new Uri(BaseUri.AbsoluteUri.Substring(0,BaseUri.AbsoluteUri.Length -4)); + } + } + + // custom startup process for this test + protected override void StartupConfiguration(Startup startup, IAppBuilder app) + { + + var configuration = startup.BuildConfiguration(); + // here we add the custom BaseUrlServcie + configuration.CustomBaseUrlService = new BaseUrlService("api"); + var configurator = startup.BuildAutofacConfigurator(app); + var httpConfig = startup.BuildHttpConfiguration(); + startup.MergeAndSetupConfiguration(app, configurator, httpConfig, configuration); + } + + // custom expected response method + protected override string ExpectedResponse(string expectedResponseTextResourcePath) + { + var expected = base.ExpectedResponse(expectedResponseTextResourcePath); + return Regex.Replace(expected, @"www\.example\.com\/", @"www.example.com/api/"); + } + + // copied some tests in here + + // copied from ComputedIdTests + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Language.csv", @"Data")] + [DeploymentItem(@"Data\LanguageUserLink.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task Get_resource_with_computed_id_by_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "language-user-links/9001_402"); + + await AssertResponseContent(response, @"Fixtures\ComputedId\Responses\Get_resource_with_computed_id_by_id_Response.json", HttpStatusCode.OK); + } + } + + + // copied from CreatingResourcesTests + + + [TestMethod] + [DeploymentItem(@"Data\PostLongId.csv", @"Data")] + public async Task PostLongId_with_client_provided_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPost(effortConnection, "post-long-ids", @"Fixtures\CreatingResources\Requests\PostLongId_with_client_provided_id_Request.json"); + + await AssertResponseContent(response, @"Fixtures\CreatingResources\Responses\PostLongId_with_client_provided_id_Response.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.PostsLongId.ToArray(); + allPosts.Length.Should().Be(5); + var actualPost = allPosts.First(t => t.Id == 205); + actualPost.Id.Should().Be(205); + actualPost.Title.Should().Be("Added post"); + actualPost.Content.Should().Be("Added post content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 03, 11, 04, 31, 0, new TimeSpan(0))); + } + } + } + + + + // copied from DeletingResourcesTests + + [TestMethod] + [DeploymentItem(@"Data\PostID.csv", @"Data")] + public async Task DeleteID() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitDelete(effortConnection, "post-i-ds/203"); + + var responseContent = await response.Content.ReadAsStringAsync(); + responseContent.Should().Be(""); + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.PostsID.ToArray(); + allPosts.Length.Should().Be(3); + var actualPosts = allPosts.FirstOrDefault(t => t.ID == "203"); + actualPosts.Should().BeNull(); + } + } + } + + + + // copied from FetchingResourcesTests + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetWithFilter() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts?filter[title]=Post 4"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\GetWithFilterResponse.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Comment.csv", @"Data")] + [DeploymentItem(@"Data\Post.csv", @"Data")] + [DeploymentItem(@"Data\PostTagLink.csv", @"Data")] + [DeploymentItem(@"Data\Tag.csv", @"Data")] + [DeploymentItem(@"Data\User.csv", @"Data")] + public async Task GetById() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/202"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\GetByIdResponse.json", HttpStatusCode.OK); + } + } + + } +} diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj index f7a3c706..b335e6c5 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj @@ -61,6 +61,10 @@ ..\packages\Microsoft.Owin.Testing.3.0.0\lib\net45\Microsoft.Owin.Testing.dll + + ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + True + ..\packages\NMemory.1.0.1\lib\net45\NMemory.dll @@ -71,7 +75,15 @@ + + ..\packages\Microsoft.AspNet.WebApi.Client.5.2.2\lib\net45\System.Net.Http.Formatting.dll + True + + + ..\packages\Microsoft.AspNet.WebApi.Core.5.2.2\lib\net45\System.Web.Http.dll + True + @@ -98,6 +110,7 @@ + @@ -107,6 +120,10 @@ {76dee472-723b-4be6-8b97-428ac326e30f} JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp + + {AF7861F3-550B-4F70-A33E-1E5F48D39333} + JSONAPI.Autofac + {52b19fd6-efaa-45b5-9c3e-a652e27608d1} JSONAPI diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/packages.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/packages.config index f704445b..0243217e 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/packages.config +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/packages.config @@ -3,9 +3,12 @@ + + + \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs index 02097fc4..130abbb8 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -32,3 +33,4 @@ // by using the '*' as shown below: [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: InternalsVisibleTo("JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests")] \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs index 371b9a22..aec594d2 100644 --- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs @@ -33,9 +33,35 @@ public Startup(Func dbContextFactory) } public void Configuration(IAppBuilder app) + { + /* these steps are divided in multiple methods to support better acceptance tests + * in production all the steps can be made inside the Configuration method. + */ + var configuration = BuildConfiguration(); + + var configurator = BuildAutofacConfigurator(app); + + var httpConfig = BuildHttpConfiguration(); + + MergeAndSetupConfiguration(app, configurator, httpConfig, configuration); + } + + internal void MergeAndSetupConfiguration(IAppBuilder app, JsonApiHttpAutofacConfigurator configurator, + HttpConfiguration httpConfig, JsonApiConfiguration configuration) + { + configurator.Apply(httpConfig, configuration); + app.UseWebApi(httpConfig); + app.UseAutofacWebApi(httpConfig); + } + + /// + /// Build up the which registers all the model types and their mappings. + /// + /// + internal JsonApiConfiguration BuildConfiguration() { var configuration = new JsonApiConfiguration( - new Core.PluralizationService( + new Core.PluralizationService( new Dictionary { { "Child", "Children" } })); configuration.RegisterEntityFrameworkResourceType(); configuration.RegisterEntityFrameworkResourceType(); @@ -65,7 +91,16 @@ public void Configuration(IAppBuilder app) configuration.RegisterResourceType(); configuration.RegisterEntityFrameworkResourceType(); configuration.RegisterEntityFrameworkResourceType(); + return configuration; + } + /// + /// Build up the which registers , modules and materializer + /// + /// + /// + internal JsonApiHttpAutofacConfigurator BuildAutofacConfigurator(IAppBuilder app) + { var configurator = new JsonApiHttpAutofacConfigurator(); configurator.OnApplicationLifetimeScopeCreating(builder => { @@ -83,7 +118,15 @@ public void Configuration(IAppBuilder app) // TODO: is this a candidate for spinning into a JSONAPI.Autofac.WebApi.Owin package? Yuck app.UseAutofacMiddleware(applicationLifetimeScope); }); + return configurator; + } + /// + /// Build up the with additional routes + /// + /// + internal HttpConfiguration BuildHttpConfiguration() + { var httpConfig = new HttpConfiguration { IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always @@ -93,12 +136,10 @@ public void Configuration(IAppBuilder app) httpConfig.Routes.MapHttpRoute("Samples", "samples", new { Controller = "Samples" }); httpConfig.Routes.MapHttpRoute("Search", "search", new { Controller = "Search" }); httpConfig.Routes.MapHttpRoute("Trees", "trees", new { Controller = "Trees" }); - - configurator.Apply(httpConfig, configuration); - app.UseWebApi(httpConfig); - app.UseAutofacWebApi(httpConfig); + return httpConfig; } + private BinaryExpression LanguageUserLinkFilterByIdFactory(ParameterExpression param, string id) { var split = id.Split('_'); diff --git a/JSONAPI.Autofac/JsonApiAutofacModule.cs b/JSONAPI.Autofac/JsonApiAutofacModule.cs index e937de35..d388dafc 100644 --- a/JSONAPI.Autofac/JsonApiAutofacModule.cs +++ b/JSONAPI.Autofac/JsonApiAutofacModule.cs @@ -126,7 +126,14 @@ protected override void Load(ContainerBuilder builder) }); builder.RegisterType().SingleInstance(); - builder.RegisterType().As().SingleInstance(); + if (_jsonApiConfiguration.CustomBaseUrlService != null) + { + builder.Register(c => _jsonApiConfiguration.CustomBaseUrlService).As().SingleInstance(); + } + else + { + builder.RegisterType().As().SingleInstance(); + } builder.RegisterType().As().InstancePerRequest(); // Serialization diff --git a/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs b/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs index 2fa79374..035f8c09 100644 --- a/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs +++ b/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs @@ -52,7 +52,7 @@ public void Apply(HttpConfiguration httpConfiguration, IJsonApiConfiguration jso _appLifetimeScopeBegunAction(applicationLifetimeScope); var jsonApiHttpConfiguration = applicationLifetimeScope.Resolve(); - jsonApiHttpConfiguration.Apply(httpConfiguration); + jsonApiHttpConfiguration.Apply(httpConfiguration, jsonApiConfiguration); httpConfiguration.DependencyResolver = new AutofacWebApiDependencyResolver(applicationLifetimeScope); } diff --git a/JSONAPI.Tests/Http/BaseUrlServiceTest.cs b/JSONAPI.Tests/Http/BaseUrlServiceTest.cs new file mode 100644 index 00000000..304ae60f --- /dev/null +++ b/JSONAPI.Tests/Http/BaseUrlServiceTest.cs @@ -0,0 +1,287 @@ +using System; +using System.Net.Http; +using FluentAssertions; +using JSONAPI.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Tests.Http +{ + [TestClass] + public class BaseUrlServiceTest + { + [TestMethod] + public void BaseUrlRootTest() + { + // Arrange + const string uri = "http://api.example.com/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(); + + // Act + var baseUrl =baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/"); + } + + [TestMethod] + public void BaseUrlOneLevelTest() + { + // Arrange + const string uri = "http://api.example.com/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService("api"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/"); + } + + [TestMethod] + public void BaseUrlOneLevelSlashTest() + { + // Arrange + const string uri = "http://api.example.com/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService("/api"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/"); + } + + [TestMethod] + public void BaseUrlOneLevelSlash2Test() + { + // Arrange + const string uri = "http://api.example.com/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService("api/"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/"); + } + + [TestMethod] + public void BaseUrlTwoLevelTest() + { + // Arrange + const string uri = "http://api.example.com/api/superapi/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService("api/superapi"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/superapi/"); + } + + [TestMethod] + public void BaseUrlTwoLevelSlashTest() + { + // Arrange + const string uri = "http://api.example.com/api/superapi/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService("api/superapi/"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/superapi/"); + } + + [TestMethod] + public void BaseUrlTwoLevelSlash2Test() + { + // Arrange + const string uri = "http://api.example.com/api/superapi/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService("/api/superapi/"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/superapi/"); + } + + [TestMethod] + public void BaseUrlConflictingNameTest() + { + // Arrange + const string uri = "http://api.example.com/api/superapi?sort=api-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService("api"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/"); + } + + + [TestMethod] + public void BaseUrlPublicOriginTest() + { + // Arrange + const string uri = "http://wwwhost123/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("http://api.example.com/"), ""); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/"); + } + + [TestMethod] + public void BaseUrlPublicOriginNoSlashTest() + { + // Arrange + const string uri = "http://wwwhost123/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("http://api.example.com"), ""); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/"); + } + + [TestMethod] + public void BaseUrlPublicOriginHttpsTest() + { + // Arrange + const string uri = "http://wwwhost123/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("https://api.example.com/"), ""); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("https://api.example.com/"); + } + + [TestMethod] + public void BaseUrlPublicOriginHttpsHighPortTest() + { + // Arrange + const string uri = "http://wwwhost123/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("https://api.example.com:12443/"), ""); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("https://api.example.com:12443/"); + } + + [TestMethod] + public void BaseUrlPublicOriginInternalPortTest() + { + // Arrange + const string uri = "http://wwwhost123:8080/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("http://api.example.com/"), ""); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/"); + } + + + + [TestMethod] + public void BaseUrlPublicOriginContextPathTest() + { + // Arrange + const string uri = "http://wwwhost123/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("http://api.example.com/"), "api"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/"); + } + + [TestMethod] + public void BaseUrlPublicOriginNoSlashContextPathTest() + { + // Arrange + const string uri = "http://wwwhost123/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("http://api.example.com"), "/api/"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/"); + } + + [TestMethod] + public void BaseUrlPublicOriginHttpsContextPathTest() + { + // Arrange + const string uri = "http://wwwhost123/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("https://api.example.com/"), "/api"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("https://api.example.com/api/"); + } + + [TestMethod] + public void BaseUrlPublicOriginHttpsHighPortContextPathTest() + { + // Arrange + const string uri = "http://wwwhost123/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("https://api.example.com:12443/"), "api"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("https://api.example.com:12443/api/"); + } + + [TestMethod] + public void BaseUrlPublicOriginInternalPortContextPathTest() + { + // Arrange + const string uri = "http://wwwhost123:8080/api/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + var baseUrlService = new BaseUrlService(new Uri("http://api.example.com/"), "api"); + + // Act + var baseUrl = baseUrlService.GetBaseUrl(request); + + // Assert + baseUrl.Should().BeEquivalentTo("http://api.example.com/api/"); + } + + + } +} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 7b2fff70..4a86feae 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -90,6 +90,7 @@ + diff --git a/JSONAPI/Configuration/JsonApiConfiguration.cs b/JSONAPI/Configuration/JsonApiConfiguration.cs index 22722ba4..27752c1a 100644 --- a/JSONAPI/Configuration/JsonApiConfiguration.cs +++ b/JSONAPI/Configuration/JsonApiConfiguration.cs @@ -14,6 +14,7 @@ public class JsonApiConfiguration : IJsonApiConfiguration private readonly IResourceTypeRegistrar _resourceTypeRegistrar; public ILinkConventions LinkConventions { get; private set; } public IEnumerable ResourceTypeConfigurations { get { return _resourceTypeConfigurations; } } + public IBaseUrlService CustomBaseUrlService { get; set; } private readonly IList _resourceTypeConfigurations; @@ -104,5 +105,10 @@ public interface IJsonApiConfiguration /// by the ResourceTypeRegistrar /// IEnumerable ResourceTypeConfigurations { get; } + + /// + /// A custom configured + /// + IBaseUrlService CustomBaseUrlService { get; } } } diff --git a/JSONAPI/Configuration/JsonApiHttpConfiguration.cs b/JSONAPI/Configuration/JsonApiHttpConfiguration.cs index 32ce73b1..4451eda8 100644 --- a/JSONAPI/Configuration/JsonApiHttpConfiguration.cs +++ b/JSONAPI/Configuration/JsonApiHttpConfiguration.cs @@ -34,7 +34,8 @@ public JsonApiHttpConfiguration(JsonApiFormatter formatter, /// Applies the running configuration to an HttpConfiguration instance /// /// The HttpConfiguration to apply this JsonApiHttpConfiguration to - public void Apply(HttpConfiguration httpConfig) + /// configuration holding BaseUrlService wich could provide a context path. + public void Apply(HttpConfiguration httpConfig, IJsonApiConfiguration jsonApiConfiguration) { httpConfig.Formatters.Clear(); httpConfig.Formatters.Add(_formatter); @@ -42,10 +43,16 @@ public void Apply(HttpConfiguration httpConfig) httpConfig.Filters.Add(_fallbackDocumentBuilderAttribute); httpConfig.Filters.Add(_jsonApiExceptionFilterAttribute); + var contextPath = jsonApiConfiguration.CustomBaseUrlService?.GetContextPath(); + if (contextPath != null && !contextPath.Equals(string.Empty)) + { + contextPath += "/"; + } + // Web API routes - httpConfig.Routes.MapHttpRoute("ResourceCollection", "{resourceType}", new { controller = "Main" }); - httpConfig.Routes.MapHttpRoute("Resource", "{resourceType}/{id}", new { controller = "Main" }); - httpConfig.Routes.MapHttpRoute("RelatedResource", "{resourceType}/{id}/{relationshipName}", new { controller = "Main" }); + httpConfig.Routes.MapHttpRoute("ResourceCollection", contextPath + "{resourceType}", new { controller = "Main" }); + httpConfig.Routes.MapHttpRoute("Resource", contextPath + "{resourceType}/{id}", new { controller = "Main" }); + httpConfig.Routes.MapHttpRoute("RelatedResource", contextPath + "{resourceType}/{id}/{relationshipName}", new { controller = "Main" }); } } } diff --git a/JSONAPI/Http/BaseUrlService.cs b/JSONAPI/Http/BaseUrlService.cs index 174e83e5..df1ee1a1 100644 --- a/JSONAPI/Http/BaseUrlService.cs +++ b/JSONAPI/Http/BaseUrlService.cs @@ -8,11 +8,108 @@ namespace JSONAPI.Http /// public class BaseUrlService : IBaseUrlService { + private string _contextPath = string.Empty; + private Uri _publicOrigin; + + /// + /// Default constructor + /// + public BaseUrlService() { } + + /// + /// Constructor which provides a context path for the routes of JSONAPI.NET + /// + /// context path for the routes + public BaseUrlService(string contextPath) + { + CleanContextPath(contextPath); + } + + /// + /// Constructor which provides a public origin host and a context path for the routes of JSONAPI.NET. + /// If only public origin is desired provide emtpy string to contextPath. + /// + /// public hostname + /// context path for the routes + public BaseUrlService(Uri publicOrigin, string contextPath) + { + CleanContextPath(contextPath); + this._publicOrigin = publicOrigin; + } + + /// + /// Retrieve the base path to provide in responses. + /// + /// + /// public virtual string GetBaseUrl(HttpRequestMessage requestMessage) { - return - new Uri(requestMessage.RequestUri.AbsoluteUri.Replace(requestMessage.RequestUri.PathAndQuery, - String.Empty)).ToString(); + string pathAndQuery; + string absolutUri = requestMessage.RequestUri.AbsoluteUri; + if (_publicOrigin != null) + { + var publicUriBuilder = new UriBuilder(absolutUri) + { + Host = _publicOrigin.Host, + Scheme = _publicOrigin.Scheme, + Port = _publicOrigin.Port + }; + absolutUri = publicUriBuilder.Uri.AbsoluteUri; + pathAndQuery = publicUriBuilder.Uri.PathAndQuery; + } + else + { + pathAndQuery = requestMessage.RequestUri.PathAndQuery; + } + pathAndQuery = RemoveFromBegin(pathAndQuery, GetContextPath()); + pathAndQuery= pathAndQuery.TrimStart('/'); + var baseUrl = RemoveFromEnd(absolutUri, pathAndQuery); + return baseUrl; + } + + /// + /// Provides the context path to serve JSONAPI.NET without leading and trailing slash. + /// + /// + public string GetContextPath() + { + return _contextPath; + } + + /// + /// Makes sure thre are no slashes at the beginnig or end. + /// + /// + private void CleanContextPath(string contextPath) + { + if (!string.IsNullOrEmpty(contextPath) && !contextPath.EndsWith("/")) + { + contextPath = contextPath.TrimEnd('/'); + } + if (!string.IsNullOrEmpty(contextPath) && contextPath.StartsWith("/")) + { + contextPath = contextPath.TrimStart('/'); + } + _contextPath = contextPath; + } + + + private string RemoveFromEnd(string input, string suffix) + { + if (input.EndsWith(suffix)) + { + return input.Substring(0, input.Length - suffix.Length); + } + return input; + } + private string RemoveFromBegin(string input, string prefix) + { + prefix = "/" + prefix; + if (input.StartsWith(prefix)) + { + return input.Substring(prefix.Length, input.Length - prefix.Length); + } + return input; } } } \ No newline at end of file diff --git a/JSONAPI/Http/IBaseUrlService.cs b/JSONAPI/Http/IBaseUrlService.cs index ac5aa9f0..dd9f3177 100644 --- a/JSONAPI/Http/IBaseUrlService.cs +++ b/JSONAPI/Http/IBaseUrlService.cs @@ -11,5 +11,11 @@ public interface IBaseUrlService /// Gets the base URL for a request /// string GetBaseUrl(HttpRequestMessage requestMessage); + + /// + /// Gets the context path JSONAPI is served under without slashes at the beginning and end. + /// + /// + string GetContextPath(); } } diff --git a/README.md b/README.md index b75c7ca0..b930f0e6 100644 --- a/README.md +++ b/README.md @@ -64,3 +64,42 @@ A `JSONAPI.IMaterializer` object can be added to that `ApiController` to broker # Didn't I read something about using Entity Framework? The classes in the `JSONAPI.EntityFramework` namespace take great advantage of the patterns set out in the `JSONAPI` namespace. The `EntityFrameworkMaterializer` is an `IMaterializer` that can operate with your own `DbContext` class to retrieve objects by Id/Primary Key, and can retrieve and update existing objects from your context in a way that Entity Framework expects for change tracking…that means, in theory, you can use the provided `JSONAPI.EntityFramework.ApiController` base class to handle GET, PUT, POST, and DELETE without writing any additional code! You will still almost certainly subclass `ApiController` to implement your business logic, but that means you only have to worry about your business logic--not implementing the JSON API spec or messing with your persistence layer. + + + +# Configuration JSONAPI.EntityFramework + +- [ ] Add some hints about the configuration of JSONAPI.EntityFramework + +## Set the context path of JSONAPI.EntityFramework + +Per default the routes created for the registered models from EntityFramework will appear in root folder. This can conflict with folders of the file system or other routes you may want to serve from the same project. +To solve the issue we can create an instance of the `BaseUrlService` and put it in the configuration. +The `BaseUrlService` can be created with the context path parameter which will be used to register the routes and put into responses. + +```C# +var configuration = new JsonApiConfiguration(); +configuration.RegisterEntityFrameworkResourceType(); +// ... registration stuff you need + +// this makes JSONAPI.NET create the route 'api/posts' +configuration.CustomBaseUrlService = new BaseUrlService("api"); +``` + +## Set the public origin host of JSONAPI +Since JSONAPI.NET returns urls it could result wrong links if JSONAPI runs behind a reverse proxy. Configure the public origin address as follows: +```C# +var configuration = new JsonApiConfiguration(); +configuration.RegisterEntityFrameworkResourceType(); +// ... registration stuff you need + +// this makes JSONAPI.NET to set the urls in responses to https://api.example.com/posts +// Important: don't leave the second string parameter! see below. +configuration.CustomBaseUrlService = new BaseUrlService(new Uri("https://api.example.com/"), ""); + +// this can also be combined with the context paht for routing like that: +// this makes JSONAPI.NET create the route 'api/posts' and response urls to be https://api.example.com:9443/api/posts +configuration.CustomBaseUrlService = new BaseUrlService(new Uri("https://api.example.com:9443/"), "api"); + +``` +