diff --git a/.gitignore b/.gitignore index fd5204b5..d9edd238 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.suo *.user *.sln.docstates +.vs/ # Build results [Dd]ebug/ diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs new file mode 100644 index 00000000..d0594337 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AcceptanceTestsBase.cs @@ -0,0 +1,186 @@ +using System; +using System.Data.Common; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Formatting; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using JSONAPI.Json; +using Microsoft.Owin.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using Owin; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public abstract class AcceptanceTestsBase + { + private const string JsonApiContentType = "application/vnd.api+json"; + 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]*"".*?"""); + protected static Uri BaseUri = new Uri("https://www.example.com"); + + protected static DbConnection GetEffortConnection() + { + return TestHelpers.GetEffortConnection(@"Data"); + } + + protected virtual async Task AssertResponseContent(HttpResponseMessage response, string expectedResponseTextResourcePath, HttpStatusCode expectedStatusCode, bool redactErrorData = false) + { + var responseContent = await response.Content.ReadAsStringAsync(); + + var expectedResponse = ExpectedResponse(expectedResponseTextResourcePath); + string actualResponse; + if (redactErrorData) + { + var redactedResponse = GuidRegex.Replace(responseContent, "{{SOME_GUID}}"); + actualResponse = StackTraceRegex.Replace(redactedResponse, "\"stackTrace\":\"{{STACK_TRACE}}\""); + actualResponse.Should().Be(expectedResponse); + } + else + { + actualResponse = responseContent; + JsonSerializerSettings settings = new JsonSerializerSettings + { + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + DateFormatString = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffff+00:00", + Culture = CultureInfo.InvariantCulture, + Formatting = Formatting.Indented + }; + + var actualResponseJObject = JsonConvert.DeserializeObject(actualResponse) as JObject; + var expectedResponseJObject = JsonConvert.DeserializeObject(expectedResponse) as JObject; + var equals = JToken.DeepEquals(actualResponseJObject, expectedResponseJObject); + if (!equals) + { + Assert.Fail("should be: " + JsonConvert.SerializeObject(expectedResponseJObject, settings) + "\n but was: " + JsonConvert.SerializeObject(actualResponseJObject, settings)); + } + } + + response.Content.Headers.ContentType.MediaType.Should().Be(JsonApiContentType); + response.Content.Headers.ContentType.CharSet.Should().Be("utf-8"); + + 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) + { + using (var server = TestServer.Create(app => + { + var startup = new Startup(() => new TestDbContext(effortConnection, false)); + StartupConfiguration(startup, app); + })) + { + var uri = new Uri(BaseUri, requestPath); + var response = await server.CreateRequest(uri.ToString()).AddHeader("Accept", JsonApiContentType).GetAsync(); + return response; + } + } + + #endregion + #region POST + + protected async Task SubmitPost(DbConnection effortConnection, string requestPath, string requestDataTextResourcePath) + { + using (var server = TestServer.Create(app => + { + var startup = new Startup(() => new TestDbContext(effortConnection, false)); + StartupConfiguration(startup, app); + })) + { + var uri = new Uri(BaseUri, requestPath); + var requestContent = TestHelpers.ReadEmbeddedFile(requestDataTextResourcePath); + var response = await server + .CreateRequest(uri.ToString()) + .AddHeader("Accept", JsonApiContentType) + .And(request => + { + request.Content = new StringContent(requestContent, Encoding.UTF8, "application/vnd.api+json"); + }) + .PostAsync(); + return response; + } + } + + #endregion + #region PATCH + + protected async Task SubmitPatch(DbConnection effortConnection, string requestPath, string requestDataTextResourcePath) + { + using (var server = TestServer.Create(app => + { + var startup = new Startup(() => new TestDbContext(effortConnection, false)); + StartupConfiguration(startup, app); + })) + { + var uri = new Uri(BaseUri, requestPath); + var requestContent = TestHelpers.ReadEmbeddedFile(requestDataTextResourcePath); + var response = await server + .CreateRequest(uri.ToString()) + .AddHeader("Accept", JsonApiContentType) + .And(request => + { + request.Content = new StringContent(requestContent, Encoding.UTF8, "application/vnd.api+json"); + }).SendAsync("PATCH"); + return response; + } + } + + #endregion + #region DELETE + + protected async Task SubmitDelete(DbConnection effortConnection, string requestPath) + { + using (var server = TestServer.Create(app => + { + var startup = new Startup(() => new TestDbContext(effortConnection, false)); + StartupConfiguration(startup, app); + })) + { + var uri = new Uri(BaseUri, requestPath); + var response = await server + .CreateRequest(uri.ToString()) + .AddHeader("Accept", JsonApiContentType) + .SendAsync("DELETE"); + return response; + } + } + + #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/App.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/App.config new file mode 100644 index 00000000..da9a789a --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/App.config @@ -0,0 +1,37 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AttributeSerializationTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AttributeSerializationTests.cs new file mode 100644 index 00000000..fd06bda9 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/AttributeSerializationTests.cs @@ -0,0 +1,21 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class AttributeSerializationTests : AcceptanceTestsBase + { + [TestMethod] + public async Task Attributes_of_various_types_serialize_correctly() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "samples"); + + await AssertResponseContent(response, @"Fixtures\AttributeSerialization\Attributes_of_various_types_serialize_correctly.json", HttpStatusCode.OK); + } + } + } +} 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/ComputedIdTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/ComputedIdTests.cs new file mode 100644 index 00000000..64c09347 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/ComputedIdTests.cs @@ -0,0 +1,46 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class ComputedIdTests : AcceptanceTestsBase + { + [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_all_of_resource_with_computed_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "language-user-links"); + + await AssertResponseContent(response, @"Fixtures\ComputedId\Responses\Get_all_of_resource_with_computed_id_Response.json", HttpStatusCode.OK); + } + } + + [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); + } + } + } +} diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs new file mode 100644 index 00000000..b189e933 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs @@ -0,0 +1,145 @@ +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class CreatingResourcesTests : AcceptanceTestsBase + { + [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 Post_with_client_provided_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPost(effortConnection, "posts", @"Fixtures\CreatingResources\Requests\Post_with_client_provided_id_Request.json"); + + await AssertResponseContent(response, @"Fixtures\CreatingResources\Responses\Post_with_client_provided_id_Response.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.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))); + actualPost.AuthorId.Should().Be("401"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Data\PostID.csv", @"Data")] + public async Task PostID_with_client_provided_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPost(effortConnection, "post-i-ds", @"Fixtures\CreatingResources\Requests\PostID_with_client_provided_id_Request.json"); + + await AssertResponseContent(response, @"Fixtures\CreatingResources\Responses\PostID_with_client_provided_id_Response.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.PostsID.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))); + } + } + } + + [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))); + } + } + } + + [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 Post_with_empty_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPost(effortConnection, "posts", @"Fixtures\CreatingResources\Requests\Post_with_empty_id_Request.json"); + + await AssertResponseContent(response, @"Fixtures\CreatingResources\Responses\Post_with_empty_id_Response.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(5); + var actualPost = allPosts.First(t => t.Id == "230"); + actualPost.Id.Should().Be("230"); + actualPost.Title.Should().Be("New post"); + actualPost.Content.Should().Be("The server generated my ID"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 04, 13, 12, 09, 0, new TimeSpan(0, 3, 0, 0))); + actualPost.AuthorId.Should().Be("401"); + } + } + } + + + [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 Post_with_empty_id_and_include() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPost(effortConnection, "posts?include=author", @"Fixtures\CreatingResources\Requests\Post_with_empty_id_Request.json"); + + await AssertResponseContent(response, @"Fixtures\CreatingResources\Responses\Post_with_empty_id_and_include_author_Response.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(5); + var actualPost = allPosts.First(t => t.Id == "230"); + actualPost.Id.Should().Be("230"); + actualPost.Title.Should().Be("New post"); + actualPost.Content.Should().Be("The server generated my ID"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 04, 13, 12, 09, 0, new TimeSpan(0, 3, 0, 0))); + actualPost.AuthorId.Should().Be("401"); + } + } + } + } +} diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Building.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Building.csv new file mode 100644 index 00000000..5faa1c59 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Building.csv @@ -0,0 +1,3 @@ +Id,Address,OwnerCompanyId +"1000","123 Sesame St.","1100" +"1001","1600 Pennsylvania Avenue", diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Child.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Child.csv new file mode 100644 index 00000000..b529aab6 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Child.csv @@ -0,0 +1,7 @@ +Id,ChildDescription,MasterId +7500,"Child 1 Description",1500 +7501,"Child 2 Description",1501 +7502,"Child 3 Description",1501 +7503,"Child 4 Description",1503 +7504,"Child 5 Description",1503 +7505,"Child 6 Description",1503 \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/Comment.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Comment.csv similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Data/Comment.csv rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Comment.csv diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Company.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Company.csv new file mode 100644 index 00000000..4dc5014b --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Company.csv @@ -0,0 +1,2 @@ +Id,Name +"1100","Big Bird and Friends" diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Language.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Language.csv new file mode 100644 index 00000000..78bcf317 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Language.csv @@ -0,0 +1,5 @@ +Id,Name +"9000","English" +"9001","French" +"9002","Spanish" +"9003","German" \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/LanguageUserLink.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/LanguageUserLink.csv new file mode 100644 index 00000000..88310b3b --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/LanguageUserLink.csv @@ -0,0 +1,7 @@ +LanguageId,UserId,FluencyLevel +"9000","401","Native" +"9001","401","Conversational" +"9002","401","Fluent" +"9001","402","Native" +"9002","403","Native" +"9003","404","Native" \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Master.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Master.csv new file mode 100644 index 00000000..2a582cc4 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Master.csv @@ -0,0 +1,5 @@ +Id,Description +1500,"Master 1 Description" +1501,"Master 2 Description" +1502,"Master 3 Description" +1503,"Master 4 Description" diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Officer.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Officer.csv new file mode 100644 index 00000000..3d36dcbe --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Officer.csv @@ -0,0 +1,6 @@ +OfficerId,Name,Rank +"12000","James T. Kirk","Captain" +"12010","Jean-Luc Picard","Captain" +"12011","William T. Riker","Commander" +"12012","Data","Lt. Commander" +"12013","Deanna Troi","Lt. Commander" \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/Post.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Post.csv similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Data/Post.csv rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Post.csv diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostID.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostID.csv new file mode 100644 index 00000000..544b39c7 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostID.csv @@ -0,0 +1,5 @@ +ID,Title,Content,Created,AuthorId +"201","Post 1","Post 1 content","2015-01-31T14:00Z" +"202","Post 2","Post 2 content","2015-02-05T08:10Z" +"203","Post 3","Post 3 content","2015-02-07T11:11Z" +"204","Post 4","Post 4 content","2015-02-08T06:59Z" \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostLongId.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostLongId.csv new file mode 100644 index 00000000..ac3f5a42 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostLongId.csv @@ -0,0 +1,5 @@ +Id,Title,Content,Created,AuthorId +201,"Post 1","Post 1 content","2015-01-31T14:00Z" +202,"Post 2","Post 2 content","2015-02-05T08:10Z" +203,"Post 3","Post 3 content","2015-02-07T11:11Z" +204,"Post 4","Post 4 content","2015-02-08T06:59Z" \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/PostTagLink.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostTagLink.csv similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Data/PostTagLink.csv rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/PostTagLink.csv diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Starship.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Starship.csv new file mode 100644 index 00000000..7214a1df --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Starship.csv @@ -0,0 +1,4 @@ +StarshipId,Name,StarshipClassId +"NCC-1701","USS Enterprise","80001" +"NCC-1701-D","USS Enterprise","80002" +"NCC-74656","USS Voyager","80003" diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/StarshipClass.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/StarshipClass.csv new file mode 100644 index 00000000..4aeb6903 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/StarshipClass.csv @@ -0,0 +1,4 @@ +StarshipClassId,Name +"80001","Constitution" +"80002","Galaxy" +"80003","Intrepid" diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/StarshipOfficerLink.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/StarshipOfficerLink.csv new file mode 100644 index 00000000..be97ff49 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/StarshipOfficerLink.csv @@ -0,0 +1,6 @@ +StarshipId,OfficerId,Position +"NCC-1701","12000","Commanding Officer" +"NCC-1701-D","12010","Commanding Officer" +"NCC-1701-D","12011","First Officer" +"NCC-1701-D","12012","Second Officer" +"NCC-1701-D","12013","Ship's Counselor" \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/Tag.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Tag.csv similarity index 100% rename from JSONAPI.EntityFramework.Tests/Acceptance/Data/Tag.csv rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/Tag.csv diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/User.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/User.csv new file mode 100644 index 00000000..d1c22ad6 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/User.csv @@ -0,0 +1,11 @@ +Id,FirstName,LastName +"401","Alice","Smith" +"402","Bob","Jones" +"403","Charlie","Michaels" +"404","Richard","Smith" +"405","Michelle","Johnson" +"406","Ed","Burns" +"407","Thomas","Potter" +"408","Pat","Morgan" +"409","Charlie","Burns" +"410","Sally","Burns" \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/UserGroup.csv b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/UserGroup.csv new file mode 100644 index 00000000..30ab4160 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Data/UserGroup.csv @@ -0,0 +1,2 @@ +Id,Name +"501","Admin users" diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/DeletingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/DeletingResourcesTests.cs new file mode 100644 index 00000000..f77715f5 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/DeletingResourcesTests.cs @@ -0,0 +1,83 @@ +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class DeletingResourcesTests : AcceptanceTestsBase + { + [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 Delete() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitDelete(effortConnection, "posts/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.Posts.ToArray(); + allPosts.Length.Should().Be(3); + var actualPosts = allPosts.FirstOrDefault(t => t.Id == "203"); + actualPosts.Should().BeNull(); + } + } + } + + [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(); + } + } + } + + [TestMethod] + [DeploymentItem(@"Data\PostLongId.csv", @"Data")] + public async Task DeleteLongId() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitDelete(effortConnection, "post-long-ids/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.PostsLongId.ToArray(); + allPosts.Length.Should().Be(3); + var actualPosts = allPosts.FirstOrDefault(t => t.Id == 203); + actualPosts.Should().BeNull(); + } + } + } + } +} diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/ErrorsTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/ErrorsTests.cs new file mode 100644 index 00000000..f1c663c6 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/ErrorsTests.cs @@ -0,0 +1,34 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class ErrorsTests : AcceptanceTestsBase + { + [TestMethod] + public async Task Controller_action_throws_exception() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "trees"); + + await AssertResponseContent(response, @"Fixtures\Errors\Controller_action_throws_exception.json", HttpStatusCode.InternalServerError, true); + } + } + + [TestMethod] + [Ignore] + public async Task Controller_does_not_exist() + { + // TODO: Currently ignoring this test because it doesn't seem possible to intercept 404s before they make it to the formatter + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "foo"); + + await AssertResponseContent(response, @"Fixtures\Errors\Controller_does_not_exist.json", HttpStatusCode.NotFound, true); + } + } + } +} diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs new file mode 100644 index 00000000..39eb2938 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs @@ -0,0 +1,251 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class FetchingResourcesTests : AcceptanceTestsBase + { + [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 GetAll() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\GetAllResponse.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 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); + } + } + + [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 Get_resource_by_id_that_doesnt_exist() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/3000"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_resource_by_id_that_doesnt_exist.json", HttpStatusCode.NotFound, true); + } + } + + [TestMethod] + [DeploymentItem(@"Data\UserGroup.csv", @"Data")] + public async Task Get_dasherized_resource() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "user-groups"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_dasherized_resource.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 Get_related_to_many() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/201/comments"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_related_to_many_response.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 Get_related_to_many_included() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/201/comments?include=author"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_related_to_many_include_response.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 Get_related_to_many_included_external() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users/401/posts?include=tags"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_related_to_many_include_external_response.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 Get_related_to_one() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/201/author"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_related_to_one_response.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 Get_included_to_one() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/201?include=author"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_included_to_one_response.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 Get_related_to_one_for_resource_that_doesnt_exist() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/3000/author"); + + await AssertResponseContent(response, + @"Fixtures\FetchingResources\Get_related_to_one_for_resource_that_doesnt_exist.json", + HttpStatusCode.NotFound, true); + } + } + + [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 Get_related_to_many_for_resource_that_doesnt_exist() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/3000/tags"); + + await AssertResponseContent(response, + @"Fixtures\FetchingResources\Get_related_to_many_for_resource_that_doesnt_exist.json", + HttpStatusCode.NotFound, true); + } + } + + [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 Get_related_resource_for_relationship_that_doesnt_exist() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts/201/bananas"); + + await AssertResponseContent(response, + @"Fixtures\FetchingResources\Get_related_resource_for_relationship_that_doesnt_exist.json", + HttpStatusCode.NotFound, true); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Building.csv", @"Data")] + [DeploymentItem(@"Data\Company.csv", @"Data")] + public async Task Get_related_to_one_where_it_is_null() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "buildings/1001/owner"); + + await AssertResponseContent(response, + @"Fixtures\FetchingResources\Get_related_to_one_where_it_is_null.json", + HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Master.csv", @"Data")] + [DeploymentItem(@"Data\Child.csv", @"Data")] + public async Task Get_related_to_many_integer_key() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "masters/1501/children"); + + await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_related_to_many_integer_key_response.json", HttpStatusCode.OK); + } + } + } +} diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json new file mode 100644 index 00000000..ca94936a --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/AttributeSerialization/Attributes_of_various_types_serialize_correctly.json @@ -0,0 +1,110 @@ +{ + "data": [ + { + "type": "samples", + "id": "1", + "attributes": { + "boolean-field": false, + "byte-field": 0, + "complex-attribute-field": null, + "date-time-field": "0001-01-01T00:00:00", + "date-time-offset-field": "0001-01-01T00:00:00.0000000+00:00", + "decimal-field": "0", + "double-field": 0.0, + "enum-field": 0, + "guid-field": "00000000-0000-0000-0000-000000000000", + "int16-field": 0, + "int32-field": 0, + "int64-field": 0, + "j-token-array-field": null, + "j-token-object-field": null, + "j-token-string-field": null, + "nullable-boolean-field": false, + "nullable-byte-field": null, + "nullable-date-time-field": null, + "nullable-date-time-offset-field": null, + "nullable-decimal-field": null, + "nullable-double-field": null, + "nullable-enum-field": null, + "nullable-guid-field": null, + "nullable-int16-field": null, + "nullable-int32-field": null, + "nullable-int64-field": null, + "nullable-sbyte-field": null, + "nullable-single-field": null, + "nullable-uint16-field": null, + "nullable-uint32-field": null, + "nullable-uint64-field": null, + "sbyte-field": 0, + "single-field": 0.0, + "string-field": null, + "uint16-field": 0, + "uint32-field": 0, + "uint64-field": 0 + } + }, + { + "type": "samples", + "id": "2", + "attributes": { + "boolean-field": true, + "byte-field": 253, + "complex-attribute-field": { + "foo": { + "baz": [ 11 ] + }, + "bar": 5 + }, + "date-time-field": "1776-07-04T00:00:00", + "date-time-offset-field": "1776-07-04T00:00:00.0000000-05:00", + "decimal-field": "1056789.123", + "double-field": 1056789.123, + "enum-field": 1, + "guid-field": "6566f9b4-5245-40de-890d-98b40a4ad656", + "int16-field": 32000, + "int32-field": 2000000000, + "int64-field": 9223372036854775807, + "j-token-array-field": [ + { + "my-field1": "George Washington", + "overridden-field2": null, + "MyField3": 216 + }, + { + "my-field1": "Thomas Jefferson", + "overridden-field2": false, + "MyField3": 631 + } + ], + "j-token-object-field": { + "my-field1": "Abraham Lincoln", + "overridden-field2": true, + "MyField3": 439 + }, + "j-token-string-field": "Andrew Jackson", + "nullable-boolean-field": true, + "nullable-byte-field": 253, + "nullable-date-time-field": "1776-07-04T00:00:00", + "nullable-date-time-offset-field": "1776-07-04T00:00:00.0000000-05:00", + "nullable-decimal-field": "1056789.123", + "nullable-double-field": 1056789.123, + "nullable-enum-field": 2, + "nullable-guid-field": "3d1fb81e-43ee-4d04-af91-c8a326341293", + "nullable-int16-field": 32000, + "nullable-int32-field": 2000000000, + "nullable-int64-field": 9223372036854775807, + "nullable-sbyte-field": 123, + "nullable-single-field": 1056789.13, + "nullable-uint16-field": 64000, + "nullable-uint32-field": 3000000000, + "nullable-uint64-field": 9223372036854775808, + "sbyte-field": 123, + "single-field": 1056789.13, + "string-field": "Some string 156", + "uint16-field": 64000, + "uint32-field": 3000000000, + "uint64-field": 9223372036854775808 + } + } + ] +} diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/ComputedId/Responses/Get_all_of_resource_with_computed_id_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/ComputedId/Responses/Get_all_of_resource_with_computed_id_Response.json new file mode 100644 index 00000000..73e17342 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/ComputedId/Responses/Get_all_of_resource_with_computed_id_Response.json @@ -0,0 +1,130 @@ +{ + "data": [ + { + "type": "language-user-links", + "id": "9000_401", + "attributes": { + "fluency-level": "Native" + }, + "relationships": { + "language": { + "links": { + "self": "https://www.example.com/language-user-links/9000_401/relationships/language", + "related": "https://www.example.com/language-user-links/9000_401/language" + } + }, + "user": { + "links": { + "self": "https://www.example.com/language-user-links/9000_401/relationships/user", + "related": "https://www.example.com/language-user-links/9000_401/user" + } + } + } + }, + { + "type": "language-user-links", + "id": "9001_401", + "attributes": { + "fluency-level": "Conversational" + }, + "relationships": { + "language": { + "links": { + "self": "https://www.example.com/language-user-links/9001_401/relationships/language", + "related": "https://www.example.com/language-user-links/9001_401/language" + } + }, + "user": { + "links": { + "self": "https://www.example.com/language-user-links/9001_401/relationships/user", + "related": "https://www.example.com/language-user-links/9001_401/user" + } + } + } + }, + { + "type": "language-user-links", + "id": "9001_402", + "attributes": { + "fluency-level": "Native" + }, + "relationships": { + "language": { + "links": { + "self": "https://www.example.com/language-user-links/9001_402/relationships/language", + "related": "https://www.example.com/language-user-links/9001_402/language" + } + }, + "user": { + "links": { + "self": "https://www.example.com/language-user-links/9001_402/relationships/user", + "related": "https://www.example.com/language-user-links/9001_402/user" + } + } + } + }, + { + "type": "language-user-links", + "id": "9002_401", + "attributes": { + "fluency-level": "Fluent" + }, + "relationships": { + "language": { + "links": { + "self": "https://www.example.com/language-user-links/9002_401/relationships/language", + "related": "https://www.example.com/language-user-links/9002_401/language" + } + }, + "user": { + "links": { + "self": "https://www.example.com/language-user-links/9002_401/relationships/user", + "related": "https://www.example.com/language-user-links/9002_401/user" + } + } + } + }, + { + "type": "language-user-links", + "id": "9002_403", + "attributes": { + "fluency-level": "Native" + }, + "relationships": { + "language": { + "links": { + "self": "https://www.example.com/language-user-links/9002_403/relationships/language", + "related": "https://www.example.com/language-user-links/9002_403/language" + } + }, + "user": { + "links": { + "self": "https://www.example.com/language-user-links/9002_403/relationships/user", + "related": "https://www.example.com/language-user-links/9002_403/user" + } + } + } + }, + { + "type": "language-user-links", + "id": "9003_404", + "attributes": { + "fluency-level": "Native" + }, + "relationships": { + "language": { + "links": { + "self": "https://www.example.com/language-user-links/9003_404/relationships/language", + "related": "https://www.example.com/language-user-links/9003_404/language" + } + }, + "user": { + "links": { + "self": "https://www.example.com/language-user-links/9003_404/relationships/user", + "related": "https://www.example.com/language-user-links/9003_404/user" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/ComputedId/Responses/Get_resource_with_computed_id_by_id_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/ComputedId/Responses/Get_resource_with_computed_id_by_id_Response.json new file mode 100644 index 00000000..fde48b61 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/ComputedId/Responses/Get_resource_with_computed_id_by_id_Response.json @@ -0,0 +1,23 @@ +{ + "data": { + "type": "language-user-links", + "id": "9001_402", + "attributes": { + "fluency-level": "Native" + }, + "relationships": { + "language": { + "links": { + "self": "https://www.example.com/language-user-links/9001_402/relationships/language", + "related": "https://www.example.com/language-user-links/9001_402/language" + } + }, + "user": { + "links": { + "self": "https://www.example.com/language-user-links/9001_402/relationships/user", + "related": "https://www.example.com/language-user-links/9001_402/user" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/PostID_with_client_provided_id_Request.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/PostID_with_client_provided_id_Request.json new file mode 100644 index 00000000..41424e02 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/PostID_with_client_provided_id_Request.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "post-i-ds", + "id": "205", + "attributes": { + "title": "Added post", + "content": "Added post content", + "created": "2015-03-11T04:31:00+00:00" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/PostLongId_with_client_provided_id_Request.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/PostLongId_with_client_provided_id_Request.json new file mode 100644 index 00000000..b52fe5c4 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/PostLongId_with_client_provided_id_Request.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "post-long-ids", + "id": "205", + "attributes": { + "title": "Added post", + "content": "Added post content", + "created": "2015-03-11T04:31:00+00:00" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/Post_with_client_provided_id_Request.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/Post_with_client_provided_id_Request.json new file mode 100644 index 00000000..c49fe508 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/Post_with_client_provided_id_Request.json @@ -0,0 +1,19 @@ +{ + "data": { + "type": "posts", + "id": "205", + "attributes": { + "title": "Added post", + "content": "Added post content", + "created": "2015-03-11T04:31:00+00:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "401" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/Post_with_empty_id_Request.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/Post_with_empty_id_Request.json new file mode 100644 index 00000000..8d2759bd --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Requests/Post_with_empty_id_Request.json @@ -0,0 +1,18 @@ +{ + "data": { + "type": "posts", + "attributes": { + "title": "New post", + "content": "The server generated my ID", + "created": "2015-04-13T12:09:00+03:00" + }, + "relationships": { + "author": { + "data": { + "type": "users", + "id": "401" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/PostID_with_client_provided_id_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/PostID_with_client_provided_id_Response.json new file mode 100644 index 00000000..7122e9e9 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/PostID_with_client_provided_id_Response.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "post-i-ds", + "id": "205", + "attributes": { + "content": "Added post content", + "created": "2015-03-11T04:31:00.0000000+00:00", + "title": "Added post" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/PostLongId_with_client_provided_id_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/PostLongId_with_client_provided_id_Response.json new file mode 100644 index 00000000..dc5ee0f8 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/PostLongId_with_client_provided_id_Response.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "post-long-ids", + "id": "205", + "attributes": { + "content": "Added post content", + "created": "2015-03-11T04:31:00.0000000+00:00", + "title": "Added post" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_client_provided_id_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_client_provided_id_Response.json new file mode 100644 index 00000000..a3050aaf --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_client_provided_id_Response.json @@ -0,0 +1,31 @@ +{ + "data": { + "type": "posts", + "id": "205", + "attributes": { + "content": "Added post content", + "created": "2015-03-11T04:31:00.0000000+00:00", + "title": "Added post" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/205/relationships/author", + "related": "https://www.example.com/posts/205/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/205/relationships/comments", + "related": "https://www.example.com/posts/205/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/205/relationships/tags", + "related": "https://www.example.com/posts/205/tags" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_empty_id_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_empty_id_Response.json new file mode 100644 index 00000000..8b7706da --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_empty_id_Response.json @@ -0,0 +1,31 @@ +{ + "data": { + "type": "posts", + "id": "230", + "attributes": { + "content": "The server generated my ID", + "created": "2015-04-13T12:09:00.0000000+03:00", + "title": "New post" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/230/relationships/author", + "related": "https://www.example.com/posts/230/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/230/relationships/comments", + "related": "https://www.example.com/posts/230/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/230/relationships/tags", + "related": "https://www.example.com/posts/230/tags" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_empty_id_and_include_author_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_empty_id_and_include_author_Response.json new file mode 100644 index 00000000..f9996946 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_empty_id_and_include_author_Response.json @@ -0,0 +1,65 @@ +{ + "data": { + "type": "posts", + "id": "230", + "attributes": { + "content": "The server generated my ID", + "created": "2015-04-13T09:09:00.0000000+00:00", + "title": "New post" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/230/relationships/author", + "related": "https://www.example.com/posts/230/author" + }, + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/230/relationships/comments", + "related": "https://www.example.com/posts/230/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/230/relationships/tags", + "related": "https://www.example.com/posts/230/tags" + } + } + } + }, + "included": [ + { + "type": "users", + "id": "401", + "attributes": { + "first-name": "Alice", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Errors/Controller_action_throws_exception.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Errors/Controller_action_throws_exception.json new file mode 100644 index 00000000..b5a6b45a --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Errors/Controller_action_throws_exception.json @@ -0,0 +1,14 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "500", + "title": "Unhandled exception", + "detail": "An unhandled exception was thrown while processing the request.", + "meta": { + "exceptionMessage": "Something bad happened!", + "stackTrace": "{{STACK_TRACE}}" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Errors/Controller_does_not_exist.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Errors/Controller_does_not_exist.json new file mode 100644 index 00000000..69c2a9c3 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Errors/Controller_does_not_exist.json @@ -0,0 +1,10 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "404", + "title": "Resource not found", + "detail": "The resource you requested does not exist." + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/GetAllResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/GetAllResponse.json new file mode 100644 index 00000000..38ba54bc --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/GetAllResponse.json @@ -0,0 +1,120 @@ +{ + "data": [ + { + "type": "posts", + "id": "201", + "attributes": { + "content": "Post 1 content", + "created": "2015-01-31T14:00:00.0000000+00:00", + "title": "Post 1" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/201/relationships/author", + "related": "https://www.example.com/posts/201/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/201/relationships/comments", + "related": "https://www.example.com/posts/201/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/201/relationships/tags", + "related": "https://www.example.com/posts/201/tags" + } + } + } + }, + { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } + } + } + }, + { + "type": "posts", + "id": "203", + "attributes": { + "content": "Post 3 content", + "created": "2015-02-07T11:11:00.0000000+00:00", + "title": "Post 3" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/203/relationships/author", + "related": "https://www.example.com/posts/203/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/203/relationships/comments", + "related": "https://www.example.com/posts/203/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/203/relationships/tags", + "related": "https://www.example.com/posts/203/tags" + } + } + } + }, + { + "type": "posts", + "id": "204", + "attributes": { + "content": "Post 4 content", + "created": "2015-02-08T06:59:00.0000000+00:00", + "title": "Post 4" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/204/relationships/author", + "related": "https://www.example.com/posts/204/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/204/relationships/comments", + "related": "https://www.example.com/posts/204/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/204/relationships/tags", + "related": "https://www.example.com/posts/204/tags" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/GetByIdResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/GetByIdResponse.json new file mode 100644 index 00000000..f9067b51 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/GetByIdResponse.json @@ -0,0 +1,31 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/GetWithFilterResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/GetWithFilterResponse.json new file mode 100644 index 00000000..7a337c39 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/GetWithFilterResponse.json @@ -0,0 +1,33 @@ +{ + "data": [ + { + "type": "posts", + "id": "204", + "attributes": { + "content": "Post 4 content", + "created": "2015-02-08T06:59:00.0000000+00:00", + "title": "Post 4" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/204/relationships/author", + "related": "https://www.example.com/posts/204/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/204/relationships/comments", + "related": "https://www.example.com/posts/204/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/204/relationships/tags", + "related": "https://www.example.com/posts/204/tags" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_dasherized_resource.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_dasherized_resource.json new file mode 100644 index 00000000..23c46576 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_dasherized_resource.json @@ -0,0 +1,19 @@ +{ + "data": [ + { + "type": "user-groups", + "id": "501", + "attributes": { + "name": "Admin users" + }, + "relationships": { + "users": { + "links": { + "self": "https://www.example.com/user-groups/501/relationships/users", + "related": "https://www.example.com/user-groups/501/users" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_included_to_one_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_included_to_one_response.json new file mode 100644 index 00000000..98fcd282 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_included_to_one_response.json @@ -0,0 +1,65 @@ +{ + "data": { + "type": "posts", + "id": "201", + "attributes": { + "content": "Post 1 content", + "created": "2015-01-31T14:00:00.0000000+00:00", + "title": "Post 1" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/201/relationships/author", + "related": "https://www.example.com/posts/201/author" + }, + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/201/relationships/comments", + "related": "https://www.example.com/posts/201/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/201/relationships/tags", + "related": "https://www.example.com/posts/201/tags" + } + } + } + }, + "included": [ + { + "type": "users", + "id": "401", + "attributes": { + "first-name": "Alice", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json new file mode 100644 index 00000000..44e502d2 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_resource_for_relationship_that_doesnt_exist.json @@ -0,0 +1,10 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "404", + "title": "Resource not found", + "detail": "No relationship `bananas` exists for the resource type `posts`." + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_for_resource_that_doesnt_exist.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_for_resource_that_doesnt_exist.json new file mode 100644 index 00000000..3921d080 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_for_resource_that_doesnt_exist.json @@ -0,0 +1,10 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "404", + "title": "Resource not found", + "detail": "No resource of type `posts` exists with id `3000`." + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_external_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_external_response.json new file mode 100644 index 00000000..e8008645 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_external_response.json @@ -0,0 +1,164 @@ +{ + "data": [ + { + "type": "posts", + "id": "201", + "attributes": { + "content": "Post 1 content", + "created": "2015-01-31T14:00:00.0000000+00:00", + "title": "Post 1" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/201/relationships/author", + "related": "https://www.example.com/posts/201/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/201/relationships/comments", + "related": "https://www.example.com/posts/201/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/201/relationships/tags", + "related": "https://www.example.com/posts/201/tags" + }, + "data": [ + { + "type": "tags", + "id": "301" + }, + { + "type": "tags", + "id": "302" + } + ] + } + } + }, + { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + }, + "data": [ + { + "type": "tags", + "id": "302" + }, + { + "type": "tags", + "id": "303" + } + ] + } + } + }, + { + "type": "posts", + "id": "203", + "attributes": { + "content": "Post 3 content", + "created": "2015-02-07T11:11:00.0000000+00:00", + "title": "Post 3" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/203/relationships/author", + "related": "https://www.example.com/posts/203/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/203/relationships/comments", + "related": "https://www.example.com/posts/203/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/203/relationships/tags", + "related": "https://www.example.com/posts/203/tags" + }, + "data": [ + { + "type": "tags", + "id": "303" + } + ] + } + } + } + ], + "included": [ + { + "type": "tags", + "id": "301", + "attributes": { + "name": "Tag A" + }, + "relationships": { + "posts": { + "links": { + "self": "https://www.example.com/tags/301/relationships/posts", + "related": "https://www.example.com/tags/301/posts" + } + } + } + }, + { + "type": "tags", + "id": "302", + "attributes": { + "name": "Tag B" + }, + "relationships": { + "posts": { + "links": { + "self": "https://www.example.com/tags/302/relationships/posts", + "related": "https://www.example.com/tags/302/posts" + } + } + } + }, + { + "type": "tags", + "id": "303", + "attributes": { + "name": "Tag C" + }, + "relationships": { + "posts": { + "links": { + "self": "https://www.example.com/tags/303/relationships/posts", + "related": "https://www.example.com/tags/303/posts" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_response.json new file mode 100644 index 00000000..e1a01091 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_response.json @@ -0,0 +1,140 @@ +{ + "data": [ + { + "type": "comments", + "id": "101", + "attributes": { + "created": "2015-01-31T14:30:00.0000000+00:00", + "text": "Comment 1" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/comments/101/relationships/author", + "related": "https://www.example.com/comments/101/author" + }, + "data": { + "type": "users", + "id": "403" + } + }, + "post": { + "links": { + "self": "https://www.example.com/comments/101/relationships/post", + "related": "https://www.example.com/comments/101/post" + } + } + } + }, + { + "type": "comments", + "id": "102", + "attributes": { + "created": "2015-01-31T14:35:00.0000000+00:00", + "text": "Comment 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/comments/102/relationships/author", + "related": "https://www.example.com/comments/102/author" + }, + "data": { + "type": "users", + "id": "402" + } + }, + "post": { + "links": { + "self": "https://www.example.com/comments/102/relationships/post", + "related": "https://www.example.com/comments/102/post" + } + } + } + }, + { + "type": "comments", + "id": "103", + "attributes": { + "created": "2015-01-31T14:41:00.0000000+00:00", + "text": "Comment 3" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/comments/103/relationships/author", + "related": "https://www.example.com/comments/103/author" + }, + "data": { + "type": "users", + "id": "403" + } + }, + "post": { + "links": { + "self": "https://www.example.com/comments/103/relationships/post", + "related": "https://www.example.com/comments/103/post" + } + } + } + } + ], + "included": [ + { + "type": "users", + "id": "403", + "attributes": { + "first-name": "Charlie", + "last-name": "Michaels" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/403/relationships/comments", + "related": "https://www.example.com/users/403/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/403/relationships/posts", + "related": "https://www.example.com/users/403/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/403/relationships/user-groups", + "related": "https://www.example.com/users/403/user-groups" + } + } + } + }, + { + "type": "users", + "id": "402", + "attributes": { + "first-name": "Bob", + "last-name": "Jones" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/402/relationships/comments", + "related": "https://www.example.com/users/402/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/402/relationships/posts", + "related": "https://www.example.com/users/402/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/402/relationships/user-groups", + "related": "https://www.example.com/users/402/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_integer_key_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_integer_key_response.json new file mode 100644 index 00000000..f99bab69 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_integer_key_response.json @@ -0,0 +1,34 @@ +{ + "data": [ + { + "type": "children", + "id": "7501", + "attributes": { + "child-description": "Child 2 Description" + }, + "relationships": { + "master": { + "links": { + "self": "https://www.example.com/children/7501/relationships/master", + "related": "https://www.example.com/children/7501/master" + } + } + } + }, + { + "type": "children", + "id": "7502", + "attributes": { + "child-description": "Child 3 Description" + }, + "relationships": { + "master": { + "links": { + "self": "https://www.example.com/children/7502/relationships/master", + "related": "https://www.example.com/children/7502/master" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_response.json new file mode 100644 index 00000000..020de840 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_response.json @@ -0,0 +1,70 @@ +{ + "data": [ + { + "type": "comments", + "id": "101", + "attributes": { + "created": "2015-01-31T14:30:00.0000000+00:00", + "text": "Comment 1" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/comments/101/relationships/author", + "related": "https://www.example.com/comments/101/author" + } + }, + "post": { + "links": { + "self": "https://www.example.com/comments/101/relationships/post", + "related": "https://www.example.com/comments/101/post" + } + } + } + }, + { + "type": "comments", + "id": "102", + "attributes": { + "created": "2015-01-31T14:35:00.0000000+00:00", + "text": "Comment 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/comments/102/relationships/author", + "related": "https://www.example.com/comments/102/author" + } + }, + "post": { + "links": { + "self": "https://www.example.com/comments/102/relationships/post", + "related": "https://www.example.com/comments/102/post" + } + } + } + }, + { + "type": "comments", + "id": "103", + "attributes": { + "created": "2015-01-31T14:41:00.0000000+00:00", + "text": "Comment 3" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/comments/103/relationships/author", + "related": "https://www.example.com/comments/103/author" + } + }, + "post": { + "links": { + "self": "https://www.example.com/comments/103/relationships/post", + "related": "https://www.example.com/comments/103/post" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_one_for_resource_that_doesnt_exist.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_one_for_resource_that_doesnt_exist.json new file mode 100644 index 00000000..3921d080 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_one_for_resource_that_doesnt_exist.json @@ -0,0 +1,10 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "404", + "title": "Resource not found", + "detail": "No resource of type `posts` exists with id `3000`." + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_one_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_one_response.json new file mode 100644 index 00000000..504bb96d --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_one_response.json @@ -0,0 +1,30 @@ +{ + "data": { + "type": "users", + "id": "401", + "attributes": { + "first-name": "Alice", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_one_where_it_is_null.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_one_where_it_is_null.json new file mode 100644 index 00000000..fd4493f0 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_one_where_it_is_null.json @@ -0,0 +1,3 @@ +{ + "data": null +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json new file mode 100644 index 00000000..3921d080 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_resource_by_id_that_doesnt_exist.json @@ -0,0 +1,10 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "404", + "title": "Resource not found", + "detail": "No resource of type `posts` exists with id `3000`." + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json new file mode 100644 index 00000000..5d153ee6 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Heterogeneous/Responses/GetSearchResultsResponse.json @@ -0,0 +1,55 @@ +{ + "data": [ + { + "type": "posts", + "id": "201", + "attributes": { + "content": "Post 1 content", + "created": "2015-01-31T14:00:00.0000000+00:00", + "title": "Post 1" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/201/relationships/author", + "related": "https://www.example.com/posts/201/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/201/relationships/comments", + "related": "https://www.example.com/posts/201/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/201/relationships/tags", + "related": "https://www.example.com/posts/201/tags" + } + } + } + }, + { + "type": "comments", + "id": "101", + "attributes": { + "created": "2015-01-31T14:30:00.0000000+00:00", + "text": "Comment 1" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/comments/101/relationships/author", + "related": "https://www.example.com/comments/101/author" + } + }, + "post": { + "links": { + "self": "https://www.example.com/comments/101/relationships/post", + "related": "https://www.example.com/comments/101/post" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_all.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_all.json new file mode 100644 index 00000000..07c6389e --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_all.json @@ -0,0 +1,70 @@ +{ + "data": [ + { + "type": "starships", + "id": "NCC-1701", + "attributes": { + "name": "USS Enterprise", + "starship-class": "Constitution" + }, + "relationships": { + "officers": { + "links": { + "self": "https://www.example.com/starships/NCC-1701/relationships/officers", + "related": "https://www.example.com/starships/NCC-1701/officers" + } + }, + "ship-counselor": { + "links": { + "self": "https://www.example.com/starships/NCC-1701/relationships/ship-counselor", + "related": "https://www.example.com/starships/NCC-1701/ship-counselor" + } + } + } + }, + { + "type": "starships", + "id": "NCC-1701-D", + "attributes": { + "name": "USS Enterprise", + "starship-class": "Galaxy" + }, + "relationships": { + "officers": { + "links": { + "self": "https://www.example.com/starships/NCC-1701-D/relationships/officers", + "related": "https://www.example.com/starships/NCC-1701-D/officers" + } + }, + "ship-counselor": { + "links": { + "self": "https://www.example.com/starships/NCC-1701-D/relationships/ship-counselor", + "related": "https://www.example.com/starships/NCC-1701-D/ship-counselor" + } + } + } + }, + { + "type": "starships", + "id": "NCC-74656", + "attributes": { + "name": "USS Voyager", + "starship-class": "Intrepid" + }, + "relationships": { + "officers": { + "links": { + "self": "https://www.example.com/starships/NCC-74656/relationships/officers", + "related": "https://www.example.com/starships/NCC-74656/officers" + } + }, + "ship-counselor": { + "links": { + "self": "https://www.example.com/starships/NCC-74656/relationships/ship-counselor", + "related": "https://www.example.com/starships/NCC-74656/ship-counselor" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_by_id.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_by_id.json new file mode 100644 index 00000000..d891b913 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_by_id.json @@ -0,0 +1,24 @@ +{ + "data": { + "type": "starships", + "id": "NCC-1701", + "attributes": { + "name": "USS Enterprise", + "starship-class": "Constitution" + }, + "relationships": { + "officers": { + "links": { + "self": "https://www.example.com/starships/NCC-1701/relationships/officers", + "related": "https://www.example.com/starships/NCC-1701/officers" + } + }, + "ship-counselor": { + "links": { + "self": "https://www.example.com/starships/NCC-1701/relationships/ship-counselor", + "related": "https://www.example.com/starships/NCC-1701/ship-counselor" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_related_to_many_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_related_to_many_response.json new file mode 100644 index 00000000..c1221a27 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_related_to_many_response.json @@ -0,0 +1,72 @@ +{ + "data": [ + { + "type": "starship-officers", + "id": "NCC-1701-D_12010", + "attributes": { + "name": "Jean-Luc Picard", + "position": "Commanding Officer", + "rank": "Captain" + }, + "relationships": { + "current-ship": { + "links": { + "self": "https://www.example.com/starship-officers/NCC-1701-D_12010/relationships/current-ship", + "related": "https://www.example.com/starship-officers/NCC-1701-D_12010/current-ship" + } + } + } + }, + { + "type": "starship-officers", + "id": "NCC-1701-D_12011", + "attributes": { + "name": "William T. Riker", + "position": "First Officer", + "rank": "Commander" + }, + "relationships": { + "current-ship": { + "links": { + "self": "https://www.example.com/starship-officers/NCC-1701-D_12011/relationships/current-ship", + "related": "https://www.example.com/starship-officers/NCC-1701-D_12011/current-ship" + } + } + } + }, + { + "type": "starship-officers", + "id": "NCC-1701-D_12012", + "attributes": { + "name": "Data", + "position": "Second Officer", + "rank": "Lt. Commander" + }, + "relationships": { + "current-ship": { + "links": { + "self": "https://www.example.com/starship-officers/NCC-1701-D_12012/relationships/current-ship", + "related": "https://www.example.com/starship-officers/NCC-1701-D_12012/current-ship" + } + } + } + }, + { + "type": "starship-officers", + "id": "NCC-1701-D_12013", + "attributes": { + "name": "Deanna Troi", + "position": "Ship's Counselor", + "rank": "Lt. Commander" + }, + "relationships": { + "current-ship": { + "links": { + "self": "https://www.example.com/starship-officers/NCC-1701-D_12013/relationships/current-ship", + "related": "https://www.example.com/starship-officers/NCC-1701-D_12013/current-ship" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_related_to_one_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_related_to_one_response.json new file mode 100644 index 00000000..f311d73c --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_related_to_one_response.json @@ -0,0 +1,19 @@ +{ + "data": { + "type": "starship-officers", + "id": "NCC-1701-D_12013", + "attributes": { + "name": "Deanna Troi", + "position": "Ship's Counselor", + "rank": "Lt. Commander" + }, + "relationships": { + "current-ship": { + "links": { + "self": "https://www.example.com/starship-officers/NCC-1701-D_12013/relationships/current-ship", + "related": "https://www.example.com/starship-officers/NCC-1701-D_12013/current-ship" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_resource_by_id_that_doesnt_exist.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_resource_by_id_that_doesnt_exist.json new file mode 100644 index 00000000..57931946 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Mapped/Responses/Get_resource_by_id_that_doesnt_exist.json @@ -0,0 +1,10 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "404", + "title": "Resource not found", + "detail": "No record exists with type `starships` and ID `NCC-asdf`." + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetAllResponsePaged-2-2.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetAllResponsePaged-2-2.json new file mode 100644 index 00000000..7bf2f5ba --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetAllResponsePaged-2-2.json @@ -0,0 +1,66 @@ +{ + "meta": { + "total-pages": 2, + "total-count": 4 + }, + "data": [ + { + "type": "posts", + "id": "203", + "attributes": { + "content": "Post 3 content", + "created": "2015-02-07T11:11:00.0000000+00:00", + "title": "Post 3" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/203/relationships/author", + "related": "https://www.example.com/posts/203/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/203/relationships/comments", + "related": "https://www.example.com/posts/203/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/203/relationships/tags", + "related": "https://www.example.com/posts/203/tags" + } + } + } + }, + { + "type": "posts", + "id": "204", + "attributes": { + "content": "Post 4 content", + "created": "2015-02-08T06:59:00.0000000+00:00", + "title": "Post 4" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/204/relationships/author", + "related": "https://www.example.com/posts/204/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/204/relationships/comments", + "related": "https://www.example.com/posts/204/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/204/relationships/tags", + "related": "https://www.example.com/posts/204/tags" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted-desc.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted-desc.json new file mode 100644 index 00000000..b65cea48 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted-desc.json @@ -0,0 +1,64 @@ +{ + "meta": { + "total-pages": 2, + "total-count": 3 + }, + "data": [ + { + "type": "users", + "id": "410", + "attributes": { + "first-name": "Sally", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/410/relationships/comments", + "related": "https://www.example.com/users/410/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/410/relationships/posts", + "related": "https://www.example.com/users/410/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/410/relationships/user-groups", + "related": "https://www.example.com/users/410/user-groups" + } + } + } + }, + { + "type": "users", + "id": "406", + "attributes": { + "first-name": "Ed", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/406/relationships/comments", + "related": "https://www.example.com/users/406/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/406/relationships/posts", + "related": "https://www.example.com/users/406/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/406/relationships/user-groups", + "related": "https://www.example.com/users/406/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted.json new file mode 100644 index 00000000..ee562655 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted.json @@ -0,0 +1,64 @@ +{ + "meta": { + "total-pages": 2, + "total-count": 3 + }, + "data": [ + { + "type": "users", + "id": "409", + "attributes": { + "first-name": "Charlie", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/409/relationships/comments", + "related": "https://www.example.com/users/409/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/409/relationships/posts", + "related": "https://www.example.com/users/409/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/409/relationships/user-groups", + "related": "https://www.example.com/users/409/user-groups" + } + } + } + }, + { + "type": "users", + "id": "406", + "attributes": { + "first-name": "Ed", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/406/relationships/comments", + "related": "https://www.example.com/users/406/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/406/relationships/posts", + "related": "https://www.example.com/users/406/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/406/relationships/user-groups", + "related": "https://www.example.com/users/406/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-2-1.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-2-1.json new file mode 100644 index 00000000..16e92908 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-2-1.json @@ -0,0 +1,36 @@ +{ + "meta": { + "total-pages": 3, + "total-count": 3 + }, + "data": [ + { + "type": "users", + "id": "409", + "attributes": { + "first-name": "Charlie", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/409/relationships/comments", + "related": "https://www.example.com/users/409/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/409/relationships/posts", + "related": "https://www.example.com/users/409/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/409/relationships/user-groups", + "related": "https://www.example.com/users/409/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json new file mode 100644 index 00000000..9355b97e --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedAscendingResponse.json @@ -0,0 +1,284 @@ +{ + "data": [ + { + "type": "users", + "id": "401", + "attributes": { + "first-name": "Alice", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } + } + }, + { + "type": "users", + "id": "402", + "attributes": { + "first-name": "Bob", + "last-name": "Jones" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/402/relationships/comments", + "related": "https://www.example.com/users/402/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/402/relationships/posts", + "related": "https://www.example.com/users/402/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/402/relationships/user-groups", + "related": "https://www.example.com/users/402/user-groups" + } + } + } + }, + { + "type": "users", + "id": "403", + "attributes": { + "first-name": "Charlie", + "last-name": "Michaels" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/403/relationships/comments", + "related": "https://www.example.com/users/403/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/403/relationships/posts", + "related": "https://www.example.com/users/403/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/403/relationships/user-groups", + "related": "https://www.example.com/users/403/user-groups" + } + } + } + }, + { + "type": "users", + "id": "409", + "attributes": { + "first-name": "Charlie", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/409/relationships/comments", + "related": "https://www.example.com/users/409/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/409/relationships/posts", + "related": "https://www.example.com/users/409/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/409/relationships/user-groups", + "related": "https://www.example.com/users/409/user-groups" + } + } + } + }, + { + "type": "users", + "id": "406", + "attributes": { + "first-name": "Ed", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/406/relationships/comments", + "related": "https://www.example.com/users/406/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/406/relationships/posts", + "related": "https://www.example.com/users/406/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/406/relationships/user-groups", + "related": "https://www.example.com/users/406/user-groups" + } + } + } + }, + { + "type": "users", + "id": "405", + "attributes": { + "first-name": "Michelle", + "last-name": "Johnson" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/405/relationships/comments", + "related": "https://www.example.com/users/405/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/405/relationships/posts", + "related": "https://www.example.com/users/405/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/405/relationships/user-groups", + "related": "https://www.example.com/users/405/user-groups" + } + } + } + }, + { + "type": "users", + "id": "408", + "attributes": { + "first-name": "Pat", + "last-name": "Morgan" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/408/relationships/comments", + "related": "https://www.example.com/users/408/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/408/relationships/posts", + "related": "https://www.example.com/users/408/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/408/relationships/user-groups", + "related": "https://www.example.com/users/408/user-groups" + } + } + } + }, + { + "type": "users", + "id": "404", + "attributes": { + "first-name": "Richard", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/404/relationships/comments", + "related": "https://www.example.com/users/404/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/404/relationships/posts", + "related": "https://www.example.com/users/404/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/404/relationships/user-groups", + "related": "https://www.example.com/users/404/user-groups" + } + } + } + }, + { + "type": "users", + "id": "410", + "attributes": { + "first-name": "Sally", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/410/relationships/comments", + "related": "https://www.example.com/users/410/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/410/relationships/posts", + "related": "https://www.example.com/users/410/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/410/relationships/user-groups", + "related": "https://www.example.com/users/410/user-groups" + } + } + } + }, + { + "type": "users", + "id": "407", + "attributes": { + "first-name": "Thomas", + "last-name": "Potter" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/407/relationships/comments", + "related": "https://www.example.com/users/407/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/407/relationships/posts", + "related": "https://www.example.com/users/407/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/407/relationships/user-groups", + "related": "https://www.example.com/users/407/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json new file mode 100644 index 00000000..34d2cff4 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByMixedDirectionResponse.json @@ -0,0 +1,284 @@ +{ + "data": [ + { + "type": "users", + "id": "410", + "attributes": { + "first-name": "Sally", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/410/relationships/comments", + "related": "https://www.example.com/users/410/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/410/relationships/posts", + "related": "https://www.example.com/users/410/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/410/relationships/user-groups", + "related": "https://www.example.com/users/410/user-groups" + } + } + } + }, + { + "type": "users", + "id": "406", + "attributes": { + "first-name": "Ed", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/406/relationships/comments", + "related": "https://www.example.com/users/406/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/406/relationships/posts", + "related": "https://www.example.com/users/406/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/406/relationships/user-groups", + "related": "https://www.example.com/users/406/user-groups" + } + } + } + }, + { + "type": "users", + "id": "409", + "attributes": { + "first-name": "Charlie", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/409/relationships/comments", + "related": "https://www.example.com/users/409/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/409/relationships/posts", + "related": "https://www.example.com/users/409/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/409/relationships/user-groups", + "related": "https://www.example.com/users/409/user-groups" + } + } + } + }, + { + "type": "users", + "id": "405", + "attributes": { + "first-name": "Michelle", + "last-name": "Johnson" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/405/relationships/comments", + "related": "https://www.example.com/users/405/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/405/relationships/posts", + "related": "https://www.example.com/users/405/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/405/relationships/user-groups", + "related": "https://www.example.com/users/405/user-groups" + } + } + } + }, + { + "type": "users", + "id": "402", + "attributes": { + "first-name": "Bob", + "last-name": "Jones" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/402/relationships/comments", + "related": "https://www.example.com/users/402/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/402/relationships/posts", + "related": "https://www.example.com/users/402/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/402/relationships/user-groups", + "related": "https://www.example.com/users/402/user-groups" + } + } + } + }, + { + "type": "users", + "id": "403", + "attributes": { + "first-name": "Charlie", + "last-name": "Michaels" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/403/relationships/comments", + "related": "https://www.example.com/users/403/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/403/relationships/posts", + "related": "https://www.example.com/users/403/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/403/relationships/user-groups", + "related": "https://www.example.com/users/403/user-groups" + } + } + } + }, + { + "type": "users", + "id": "408", + "attributes": { + "first-name": "Pat", + "last-name": "Morgan" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/408/relationships/comments", + "related": "https://www.example.com/users/408/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/408/relationships/posts", + "related": "https://www.example.com/users/408/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/408/relationships/user-groups", + "related": "https://www.example.com/users/408/user-groups" + } + } + } + }, + { + "type": "users", + "id": "407", + "attributes": { + "first-name": "Thomas", + "last-name": "Potter" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/407/relationships/comments", + "related": "https://www.example.com/users/407/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/407/relationships/posts", + "related": "https://www.example.com/users/407/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/407/relationships/user-groups", + "related": "https://www.example.com/users/407/user-groups" + } + } + } + }, + { + "type": "users", + "id": "404", + "attributes": { + "first-name": "Richard", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/404/relationships/comments", + "related": "https://www.example.com/users/404/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/404/relationships/posts", + "related": "https://www.example.com/users/404/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/404/relationships/user-groups", + "related": "https://www.example.com/users/404/user-groups" + } + } + } + }, + { + "type": "users", + "id": "401", + "attributes": { + "first-name": "Alice", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json new file mode 100644 index 00000000..bb1e4fbe --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByMultipleAscendingResponse.json @@ -0,0 +1,284 @@ +{ + "data": [ + { + "type": "users", + "id": "409", + "attributes": { + "first-name": "Charlie", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/409/relationships/comments", + "related": "https://www.example.com/users/409/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/409/relationships/posts", + "related": "https://www.example.com/users/409/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/409/relationships/user-groups", + "related": "https://www.example.com/users/409/user-groups" + } + } + } + }, + { + "type": "users", + "id": "406", + "attributes": { + "first-name": "Ed", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/406/relationships/comments", + "related": "https://www.example.com/users/406/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/406/relationships/posts", + "related": "https://www.example.com/users/406/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/406/relationships/user-groups", + "related": "https://www.example.com/users/406/user-groups" + } + } + } + }, + { + "type": "users", + "id": "410", + "attributes": { + "first-name": "Sally", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/410/relationships/comments", + "related": "https://www.example.com/users/410/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/410/relationships/posts", + "related": "https://www.example.com/users/410/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/410/relationships/user-groups", + "related": "https://www.example.com/users/410/user-groups" + } + } + } + }, + { + "type": "users", + "id": "405", + "attributes": { + "first-name": "Michelle", + "last-name": "Johnson" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/405/relationships/comments", + "related": "https://www.example.com/users/405/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/405/relationships/posts", + "related": "https://www.example.com/users/405/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/405/relationships/user-groups", + "related": "https://www.example.com/users/405/user-groups" + } + } + } + }, + { + "type": "users", + "id": "402", + "attributes": { + "first-name": "Bob", + "last-name": "Jones" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/402/relationships/comments", + "related": "https://www.example.com/users/402/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/402/relationships/posts", + "related": "https://www.example.com/users/402/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/402/relationships/user-groups", + "related": "https://www.example.com/users/402/user-groups" + } + } + } + }, + { + "type": "users", + "id": "403", + "attributes": { + "first-name": "Charlie", + "last-name": "Michaels" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/403/relationships/comments", + "related": "https://www.example.com/users/403/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/403/relationships/posts", + "related": "https://www.example.com/users/403/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/403/relationships/user-groups", + "related": "https://www.example.com/users/403/user-groups" + } + } + } + }, + { + "type": "users", + "id": "408", + "attributes": { + "first-name": "Pat", + "last-name": "Morgan" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/408/relationships/comments", + "related": "https://www.example.com/users/408/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/408/relationships/posts", + "related": "https://www.example.com/users/408/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/408/relationships/user-groups", + "related": "https://www.example.com/users/408/user-groups" + } + } + } + }, + { + "type": "users", + "id": "407", + "attributes": { + "first-name": "Thomas", + "last-name": "Potter" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/407/relationships/comments", + "related": "https://www.example.com/users/407/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/407/relationships/posts", + "related": "https://www.example.com/users/407/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/407/relationships/user-groups", + "related": "https://www.example.com/users/407/user-groups" + } + } + } + }, + { + "type": "users", + "id": "401", + "attributes": { + "first-name": "Alice", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } + } + }, + { + "type": "users", + "id": "404", + "attributes": { + "first-name": "Richard", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/404/relationships/comments", + "related": "https://www.example.com/users/404/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/404/relationships/posts", + "related": "https://www.example.com/users/404/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/404/relationships/user-groups", + "related": "https://www.example.com/users/404/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json new file mode 100644 index 00000000..c56db237 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByMultipleDescendingResponse.json @@ -0,0 +1,284 @@ +{ + "data": [ + { + "type": "users", + "id": "404", + "attributes": { + "first-name": "Richard", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/404/relationships/comments", + "related": "https://www.example.com/users/404/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/404/relationships/posts", + "related": "https://www.example.com/users/404/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/404/relationships/user-groups", + "related": "https://www.example.com/users/404/user-groups" + } + } + } + }, + { + "type": "users", + "id": "401", + "attributes": { + "first-name": "Alice", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } + } + }, + { + "type": "users", + "id": "407", + "attributes": { + "first-name": "Thomas", + "last-name": "Potter" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/407/relationships/comments", + "related": "https://www.example.com/users/407/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/407/relationships/posts", + "related": "https://www.example.com/users/407/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/407/relationships/user-groups", + "related": "https://www.example.com/users/407/user-groups" + } + } + } + }, + { + "type": "users", + "id": "408", + "attributes": { + "first-name": "Pat", + "last-name": "Morgan" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/408/relationships/comments", + "related": "https://www.example.com/users/408/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/408/relationships/posts", + "related": "https://www.example.com/users/408/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/408/relationships/user-groups", + "related": "https://www.example.com/users/408/user-groups" + } + } + } + }, + { + "type": "users", + "id": "403", + "attributes": { + "first-name": "Charlie", + "last-name": "Michaels" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/403/relationships/comments", + "related": "https://www.example.com/users/403/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/403/relationships/posts", + "related": "https://www.example.com/users/403/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/403/relationships/user-groups", + "related": "https://www.example.com/users/403/user-groups" + } + } + } + }, + { + "type": "users", + "id": "402", + "attributes": { + "first-name": "Bob", + "last-name": "Jones" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/402/relationships/comments", + "related": "https://www.example.com/users/402/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/402/relationships/posts", + "related": "https://www.example.com/users/402/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/402/relationships/user-groups", + "related": "https://www.example.com/users/402/user-groups" + } + } + } + }, + { + "type": "users", + "id": "405", + "attributes": { + "first-name": "Michelle", + "last-name": "Johnson" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/405/relationships/comments", + "related": "https://www.example.com/users/405/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/405/relationships/posts", + "related": "https://www.example.com/users/405/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/405/relationships/user-groups", + "related": "https://www.example.com/users/405/user-groups" + } + } + } + }, + { + "type": "users", + "id": "410", + "attributes": { + "first-name": "Sally", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/410/relationships/comments", + "related": "https://www.example.com/users/410/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/410/relationships/posts", + "related": "https://www.example.com/users/410/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/410/relationships/user-groups", + "related": "https://www.example.com/users/410/user-groups" + } + } + } + }, + { + "type": "users", + "id": "406", + "attributes": { + "first-name": "Ed", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/406/relationships/comments", + "related": "https://www.example.com/users/406/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/406/relationships/posts", + "related": "https://www.example.com/users/406/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/406/relationships/user-groups", + "related": "https://www.example.com/users/406/user-groups" + } + } + } + }, + { + "type": "users", + "id": "409", + "attributes": { + "first-name": "Charlie", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/409/relationships/comments", + "related": "https://www.example.com/users/409/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/409/relationships/posts", + "related": "https://www.example.com/users/409/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/409/relationships/user-groups", + "related": "https://www.example.com/users/409/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json new file mode 100644 index 00000000..a3541ef0 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedBySameColumnTwiceResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Attribute specified more than once", + "detail": "The attribute \"first-name\" was specified more than once.", + "source": { + "parameter": "sort" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json new file mode 100644 index 00000000..5884cc7a --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedByUnknownColumnResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Attribute not found", + "detail": "The attribute \"foobar\" does not exist on type \"users\".", + "source": { + "parameter": "sort" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json new file mode 100644 index 00000000..aa6cd74b --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Sorting/Responses/GetSortedDescendingResponse.json @@ -0,0 +1,284 @@ +{ + "data": [ + { + "type": "users", + "id": "407", + "attributes": { + "first-name": "Thomas", + "last-name": "Potter" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/407/relationships/comments", + "related": "https://www.example.com/users/407/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/407/relationships/posts", + "related": "https://www.example.com/users/407/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/407/relationships/user-groups", + "related": "https://www.example.com/users/407/user-groups" + } + } + } + }, + { + "type": "users", + "id": "410", + "attributes": { + "first-name": "Sally", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/410/relationships/comments", + "related": "https://www.example.com/users/410/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/410/relationships/posts", + "related": "https://www.example.com/users/410/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/410/relationships/user-groups", + "related": "https://www.example.com/users/410/user-groups" + } + } + } + }, + { + "type": "users", + "id": "404", + "attributes": { + "first-name": "Richard", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/404/relationships/comments", + "related": "https://www.example.com/users/404/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/404/relationships/posts", + "related": "https://www.example.com/users/404/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/404/relationships/user-groups", + "related": "https://www.example.com/users/404/user-groups" + } + } + } + }, + { + "type": "users", + "id": "408", + "attributes": { + "first-name": "Pat", + "last-name": "Morgan" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/408/relationships/comments", + "related": "https://www.example.com/users/408/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/408/relationships/posts", + "related": "https://www.example.com/users/408/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/408/relationships/user-groups", + "related": "https://www.example.com/users/408/user-groups" + } + } + } + }, + { + "type": "users", + "id": "405", + "attributes": { + "first-name": "Michelle", + "last-name": "Johnson" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/405/relationships/comments", + "related": "https://www.example.com/users/405/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/405/relationships/posts", + "related": "https://www.example.com/users/405/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/405/relationships/user-groups", + "related": "https://www.example.com/users/405/user-groups" + } + } + } + }, + { + "type": "users", + "id": "406", + "attributes": { + "first-name": "Ed", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/406/relationships/comments", + "related": "https://www.example.com/users/406/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/406/relationships/posts", + "related": "https://www.example.com/users/406/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/406/relationships/user-groups", + "related": "https://www.example.com/users/406/user-groups" + } + } + } + }, + { + "type": "users", + "id": "403", + "attributes": { + "first-name": "Charlie", + "last-name": "Michaels" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/403/relationships/comments", + "related": "https://www.example.com/users/403/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/403/relationships/posts", + "related": "https://www.example.com/users/403/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/403/relationships/user-groups", + "related": "https://www.example.com/users/403/user-groups" + } + } + } + }, + { + "type": "users", + "id": "409", + "attributes": { + "first-name": "Charlie", + "last-name": "Burns" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/409/relationships/comments", + "related": "https://www.example.com/users/409/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/409/relationships/posts", + "related": "https://www.example.com/users/409/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/409/relationships/user-groups", + "related": "https://www.example.com/users/409/user-groups" + } + } + } + }, + { + "type": "users", + "id": "402", + "attributes": { + "first-name": "Bob", + "last-name": "Jones" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/402/relationships/comments", + "related": "https://www.example.com/users/402/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/402/relationships/posts", + "related": "https://www.example.com/users/402/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/402/relationships/user-groups", + "related": "https://www.example.com/users/402/user-groups" + } + } + } + }, + { + "type": "users", + "id": "401", + "attributes": { + "first-name": "Alice", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithArrayForToOneLinkageRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithArrayForToOneLinkageRequest.json new file mode 100644 index 00000000..d77552d2 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithArrayForToOneLinkageRequest.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": { + "data": [ { "type": "users", "id": "403" } ] + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithArrayRelationshipValueRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithArrayRelationshipValueRequest.json new file mode 100644 index 00000000..7835ed54 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithArrayRelationshipValueRequest.json @@ -0,0 +1,9 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": ["301"] + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequest.json new file mode 100644 index 00000000..4a0636e6 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequest.json @@ -0,0 +1,9 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "title": "New post title" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequestID.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequestID.json new file mode 100644 index 00000000..ac7fbb4b --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequestID.json @@ -0,0 +1,9 @@ +{ + "data": { + "type": "post-i-ds", + "id": "202", + "attributes": { + "title": "New post title" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequestLongId.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequestLongId.json new file mode 100644 index 00000000..7358d7a8 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithAttributeUpdateRequestLongId.json @@ -0,0 +1,9 @@ +{ + "data": { + "type": "post-long-ids", + "id": "202", + "attributes": { + "title": "New post title" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithMissingToManyLinkageRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithMissingToManyLinkageRequest.json new file mode 100644 index 00000000..417fe5a3 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithMissingToManyLinkageRequest.json @@ -0,0 +1,10 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithMissingToOneLinkageRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithMissingToOneLinkageRequest.json new file mode 100644 index 00000000..a664a866 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithMissingToOneLinkageRequest.json @@ -0,0 +1,10 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": { + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithNullForToManyLinkageRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithNullForToManyLinkageRequest.json new file mode 100644 index 00000000..58bf45a3 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithNullForToManyLinkageRequest.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": null + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithNullToOneUpdateRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithNullToOneUpdateRequest.json new file mode 100644 index 00000000..b73bceeb --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithNullToOneUpdateRequest.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": { + "data": null + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithObjectForToManyLinkageRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithObjectForToManyLinkageRequest.json new file mode 100644 index 00000000..b1e90ab0 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithObjectForToManyLinkageRequest.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": { "type": "tags", "id": "301" } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithStringForToManyLinkageRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithStringForToManyLinkageRequest.json new file mode 100644 index 00000000..7aa288c5 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithStringForToManyLinkageRequest.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": "301" + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithStringForToOneLinkageRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithStringForToOneLinkageRequest.json new file mode 100644 index 00000000..919ab261 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithStringForToOneLinkageRequest.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": { + "data": "403" + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithStringRelationshipValueRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithStringRelationshipValueRequest.json new file mode 100644 index 00000000..5886abfe --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithStringRelationshipValueRequest.json @@ -0,0 +1,9 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": "301" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json new file mode 100644 index 00000000..19eb21a7 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyEmptyLinkageUpdateRequest.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": [] + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json new file mode 100644 index 00000000..a3adc581 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyHomogeneousDataUpdateRequest.json @@ -0,0 +1,20 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": [ + { + "id": "301", + "type": "tags" + }, + { + "id": "303", + "type": "tags" + } + ] + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json new file mode 100644 index 00000000..5d36b4cb --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingIdRequest.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": [ { "type": "tags" } ] + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json new file mode 100644 index 00000000..6b4c11ae --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyLinkageObjectMissingTypeRequest.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": [ { "id": "301" } ] + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyUpdateRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyUpdateRequest.json new file mode 100644 index 00000000..db0ef77d --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToManyUpdateRequest.json @@ -0,0 +1,13 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "tags": { + "data": [ + { "type": "tags", "id": "301" } + ] + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json new file mode 100644 index 00000000..ebdfb7d6 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingIdRequest.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": { + "data": { "type": "users" } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json new file mode 100644 index 00000000..0ef65f96 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToOneLinkageObjectMissingTypeRequest.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": { + "data": { "id": "403" } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToOneUpdateRequest.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToOneUpdateRequest.json new file mode 100644 index 00000000..296c122a --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/PatchWithToOneUpdateRequest.json @@ -0,0 +1,14 @@ +{ + "data": { + "type": "posts", + "id": "202", + "relationships": { + "author": { + "data": { + "type": "users", + "id": "403" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/Patch_with_unknown_attribute_Request.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/Patch_with_unknown_attribute_Request.json new file mode 100644 index 00000000..3394d6df --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/Patch_with_unknown_attribute_Request.json @@ -0,0 +1,10 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "title": "New post title", + "some-fake-attribute": 99 + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/Patch_with_unknown_relationship_Request.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/Patch_with_unknown_relationship_Request.json new file mode 100644 index 00000000..1b4d99fb --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Requests/Patch_with_unknown_relationship_Request.json @@ -0,0 +1,14 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "title": "New post title" + }, + "relationships": { + "some-fake-relationship": { + "data": { "type": "author", "id": "45000" } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithArrayForToOneLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithArrayForToOneLinkageResponse.json new file mode 100644 index 00000000..d7a626e3 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithArrayForToOneLinkageResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Invalid linkage for to-one relationship", + "detail": "Expected an object or null for to-one linkage", + "source": { + "pointer": "/data/relationships/author/data" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithArrayRelationshipValueResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithArrayRelationshipValueResponse.json new file mode 100644 index 00000000..56a27ad3 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithArrayRelationshipValueResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Invalid relationship object", + "detail": "Expected an object, but found StartArray", + "source": { + "pointer": "/data/relationships/tags" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponse.json new file mode 100644 index 00000000..aba0d13d --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponse.json @@ -0,0 +1,31 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "New post title" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponseID.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponseID.json new file mode 100644 index 00000000..b0448cb5 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponseID.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "post-i-ds", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "New post title" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponseLongId.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponseLongId.json new file mode 100644 index 00000000..fea5fa0a --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateResponseLongId.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "post-long-ids", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "New post title" + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateWithIncludeResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateWithIncludeResponse.json new file mode 100644 index 00000000..6bff40d9 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateWithIncludeResponse.json @@ -0,0 +1,65 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "New post title" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + }, + "data": { + "type": "users", + "id": "401" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } + } + } + }, + "included": [ + { + "type": "users", + "id": "401", + "attributes": { + "first-name": "Alice", + "last-name": "Smith" + }, + "relationships": { + "comments": { + "links": { + "self": "https://www.example.com/users/401/relationships/comments", + "related": "https://www.example.com/users/401/comments" + } + }, + "posts": { + "links": { + "self": "https://www.example.com/users/401/relationships/posts", + "related": "https://www.example.com/users/401/posts" + } + }, + "user-groups": { + "links": { + "self": "https://www.example.com/users/401/relationships/user-groups", + "related": "https://www.example.com/users/401/user-groups" + } + } + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithMissingToManyLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithMissingToManyLinkageResponse.json new file mode 100644 index 00000000..09cdf2e3 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithMissingToManyLinkageResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Missing linkage for to-many relationship", + "detail": "Expected an array for to-many linkage, but no linkage was specified.", + "source": { + "pointer": "/data/relationships/tags" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithMissingToOneLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithMissingToOneLinkageResponse.json new file mode 100644 index 00000000..f279160e --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithMissingToOneLinkageResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Missing linkage for to-one relationship", + "detail": "Expected an object for to-one linkage, but no linkage was specified.", + "source": { + "pointer": "/data/relationships/author" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithNullForToManyLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithNullForToManyLinkageResponse.json new file mode 100644 index 00000000..fc7f2409 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithNullForToManyLinkageResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Invalid linkage for to-many relationship", + "detail": "Expected an array for to-many linkage.", + "source": { + "pointer": "/data/relationships/tags/data" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithNullToOneUpdateResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithNullToOneUpdateResponse.json new file mode 100644 index 00000000..f9067b51 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithNullToOneUpdateResponse.json @@ -0,0 +1,31 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithObjectForToManyLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithObjectForToManyLinkageResponse.json new file mode 100644 index 00000000..fc7f2409 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithObjectForToManyLinkageResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Invalid linkage for to-many relationship", + "detail": "Expected an array for to-many linkage.", + "source": { + "pointer": "/data/relationships/tags/data" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithStringForToManyLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithStringForToManyLinkageResponse.json new file mode 100644 index 00000000..3326563d --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithStringForToManyLinkageResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Invalid linkage for relationship", + "detail": "Expected an array, object, or null for linkage, but got String", + "source": { + "pointer": "/data/relationships/tags/data" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithStringForToOneLinkageResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithStringForToOneLinkageResponse.json new file mode 100644 index 00000000..cfe637ad --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithStringForToOneLinkageResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Invalid linkage for relationship", + "detail": "Expected an array, object, or null for linkage, but got String", + "source": { + "pointer": "/data/relationships/author/data" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithStringRelationshipValueResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithStringRelationshipValueResponse.json new file mode 100644 index 00000000..0a717105 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithStringRelationshipValueResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Invalid relationship object", + "detail": "Expected an object, but found String", + "source": { + "pointer": "/data/relationships/author" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json new file mode 100644 index 00000000..f9067b51 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyEmptyLinkageUpdateResponse.json @@ -0,0 +1,31 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json new file mode 100644 index 00000000..f9067b51 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyHomogeneousDataUpdateResponse.json @@ -0,0 +1,31 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json new file mode 100644 index 00000000..8b28cb3f --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingIdResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Resource identifier missing id", + "detail": "The `id` key is missing.", + "source": { + "pointer": "/data/relationships/tags/data" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json new file mode 100644 index 00000000..125ef5db --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyLinkageObjectMissingTypeResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Resource identifier missing type", + "detail": "The `type` key is missing.", + "source": { + "pointer": "/data/relationships/tags/data" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyUpdateResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyUpdateResponse.json new file mode 100644 index 00000000..f9067b51 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToManyUpdateResponse.json @@ -0,0 +1,31 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json new file mode 100644 index 00000000..2beea4ec --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingIdResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Resource identifier missing id", + "detail": "The `id` key is missing.", + "source": { + "pointer": "/data/relationships/author/data" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json new file mode 100644 index 00000000..fb14417c --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToOneLinkageObjectMissingTypeResponse.json @@ -0,0 +1,13 @@ +{ + "errors": [ + { + "id": "{{SOME_GUID}}", + "status": "400", + "title": "Resource identifier missing type", + "detail": "The `type` key is missing.", + "source": { + "pointer": "/data/relationships/author/data" + } + } + ] +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToOneUpdateResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToOneUpdateResponse.json new file mode 100644 index 00000000..f9067b51 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithToOneUpdateResponse.json @@ -0,0 +1,31 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "Post 2" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/Patch_with_unknown_attribute_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/Patch_with_unknown_attribute_Response.json new file mode 100644 index 00000000..aba0d13d --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/Patch_with_unknown_attribute_Response.json @@ -0,0 +1,31 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "New post title" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/Patch_with_unknown_relationship_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/Patch_with_unknown_relationship_Response.json new file mode 100644 index 00000000..aba0d13d --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/Patch_with_unknown_relationship_Response.json @@ -0,0 +1,31 @@ +{ + "data": { + "type": "posts", + "id": "202", + "attributes": { + "content": "Post 2 content", + "created": "2015-02-05T08:10:00.0000000+00:00", + "title": "New post title" + }, + "relationships": { + "author": { + "links": { + "self": "https://www.example.com/posts/202/relationships/author", + "related": "https://www.example.com/posts/202/author" + } + }, + "comments": { + "links": { + "self": "https://www.example.com/posts/202/relationships/comments", + "related": "https://www.example.com/posts/202/comments" + } + }, + "tags": { + "links": { + "self": "https://www.example.com/posts/202/relationships/tags", + "related": "https://www.example.com/posts/202/tags" + } + } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/HeterogeneousTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/HeterogeneousTests.cs new file mode 100644 index 00000000..013baa6b --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/HeterogeneousTests.cs @@ -0,0 +1,26 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class HeterogeneousTests : AcceptanceTestsBase + { + [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 Get() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "search?s=1"); + + await AssertResponseContent(response, @"Fixtures\Heterogeneous\Responses\GetSearchResultsResponse.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 new file mode 100644 index 00000000..35d82430 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj @@ -0,0 +1,321 @@ + + + + Debug + AnyCPU + {58AEF8B8-8D51-4175-AC96-BC622703E8BB} + Library + Properties + JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests + JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests + v4.5 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + ..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Effort.EF6.1.1.4\lib\net45\Effort.dll + + + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + + + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + + + ..\packages\FluentAssertions.3.2.2\lib\net45\FluentAssertions.dll + + + ..\packages\FluentAssertions.3.2.2\lib\net45\FluentAssertions.Core.dll + + + ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + + + ..\packages\Microsoft.Owin.Hosting.3.0.0\lib\net45\Microsoft.Owin.Hosting.dll + + + ..\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 + + + ..\packages\Owin.1.0\lib\net40\Owin.dll + + + + + + + ..\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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {76dee472-723b-4be6-8b97-428ac326e30f} + JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp + + + {AF7861F3-550B-4F70-A33E-1E5F48D39333} + JSONAPI.Autofac + + + {52b19fd6-efaa-45b5-9c3e-a652e27608d1} + JSONAPI + + + + + Always + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + + + False + + + False + + + False + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/MappedTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/MappedTests.cs new file mode 100644 index 00000000..9ecf7f77 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/MappedTests.cs @@ -0,0 +1,79 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class MappedTests : AcceptanceTestsBase + { + [TestMethod] + [DeploymentItem(@"Data\Starship.csv", @"Data")] + [DeploymentItem(@"Data\StarshipClass.csv", @"Data")] + public async Task Get_all() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "starships"); + + await AssertResponseContent(response, @"Fixtures\Mapped\Responses\Get_all.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Starship.csv", @"Data")] + [DeploymentItem(@"Data\StarshipClass.csv", @"Data")] + public async Task Get_by_id() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "starships/NCC-1701"); + + await AssertResponseContent(response, @"Fixtures\Mapped\Responses\Get_by_id.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Starship.csv", @"Data")] + [DeploymentItem(@"Data\StarshipClass.csv", @"Data")] + public async Task Get_resource_by_id_that_doesnt_exist() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "starships/NCC-asdf"); + + await AssertResponseContent(response, @"Fixtures\Mapped\Responses\Get_resource_by_id_that_doesnt_exist.json", HttpStatusCode.NotFound, true); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Starship.csv", @"Data")] + [DeploymentItem(@"Data\StarshipClass.csv", @"Data")] + [DeploymentItem(@"Data\Officer.csv", @"Data")] + [DeploymentItem(@"Data\StarshipOfficerLink.csv", @"Data")] + public async Task Get_related_to_many() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "starships/NCC-1701-D/officers"); + + await AssertResponseContent(response, @"Fixtures\Mapped\Responses\Get_related_to_many_response.json", HttpStatusCode.OK); + } + } + + [TestMethod] + [DeploymentItem(@"Data\Starship.csv", @"Data")] + [DeploymentItem(@"Data\StarshipClass.csv", @"Data")] + [DeploymentItem(@"Data\Officer.csv", @"Data")] + [DeploymentItem(@"Data\StarshipOfficerLink.csv", @"Data")] + public async Task Get_related_to_one() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "starships/NCC-1701-D/ship-counselor"); + + await AssertResponseContent(response, @"Fixtures\Mapped\Responses\Get_related_to_one_response.json", HttpStatusCode.OK); + } + } + } +} diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/PaginationTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/PaginationTests.cs new file mode 100644 index 00000000..0db20f57 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/PaginationTests.cs @@ -0,0 +1,77 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class PaginationTests : AcceptanceTestsBase + { + [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 GetPage2Post2() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "posts?page[number]=1&page[size]=2"); + + await AssertResponseContent(response, @"Fixtures\Pagination\GetAllResponsePaged-2-2.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 GetWithFilter() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?filter[last-name]=Burns&page[number]=1&page[size]=1"); + + await AssertResponseContent(response, @"Fixtures\Pagination\GetFilterPaged-2-1.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 GetWithFilterSortedAscTest() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?filter[last-name]=Burns&page[number]=0&page[size]=2&sort=first-name"); + + await AssertResponseContent(response, @"Fixtures\Pagination\GetFilterPaged-1-2-sorted.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 GetWithFilterSortedDescTest() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?filter[last-name]=Burns&page[number]=0&page[size]=2&sort=-first-name"); + + await AssertResponseContent(response, @"Fixtures\Pagination\GetFilterPaged-1-2-sorted-desc.json", HttpStatusCode.OK); + } + } + + } +} diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Properties/AssemblyInfo.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..af90b244 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("3d646890-c7b9-4a90-9706-eb8378591814")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/SortingTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/SortingTests.cs new file mode 100644 index 00000000..7e11a58e --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/SortingTests.cs @@ -0,0 +1,122 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class SortingTests : AcceptanceTestsBase + { + [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 GetSortedAscending() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?sort=first-name"); + + await AssertResponseContent(response, @"Fixtures\Sorting\Responses\GetSortedAscendingResponse.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 GetSortedDesending() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?sort=-first-name"); + + await AssertResponseContent(response, @"Fixtures\Sorting\Responses\GetSortedDescendingResponse.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 GetSortedByMultipleAscending() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?sort=last-name,first-name"); + + await AssertResponseContent(response, @"Fixtures\Sorting\Responses\GetSortedByMultipleAscendingResponse.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 GetSortedByMultipleDescending() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?sort=-last-name,-first-name"); + + await AssertResponseContent(response, @"Fixtures\Sorting\Responses\GetSortedByMultipleDescendingResponse.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 GetSortedByMixedDirection() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?sort=last-name,-first-name"); + + await AssertResponseContent(response, @"Fixtures\Sorting\Responses\GetSortedByMixedDirectionResponse.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 GetSortedByUnknownColumn() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?sort=foobar"); + + await AssertResponseContent(response, @"Fixtures\Sorting\Responses\GetSortedByUnknownColumnResponse.json", HttpStatusCode.BadRequest, true); + } + } + + [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 GetSortedBySameColumnTwice() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitGet(effortConnection, "users?sort=first-name,first-name"); + + await AssertResponseContent(response, @"Fixtures\Sorting\Responses\GetSortedBySameColumnTwiceResponse.json", HttpStatusCode.BadRequest, true); + } + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/TestHelpers.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/TestHelpers.cs similarity index 59% rename from JSONAPI.EntityFramework.Tests/TestHelpers.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/TestHelpers.cs index 3411ffdd..dbafdaee 100644 --- a/JSONAPI.EntityFramework.Tests/TestHelpers.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/TestHelpers.cs @@ -5,13 +5,21 @@ using Effort; using Effort.DataLoaders; -namespace JSONAPI.EntityFramework.Tests +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests { internal static class TestHelpers { + // http://stackoverflow.com/questions/21175713/no-entity-framework-provider-found-for-the-ado-net-provider-with-invariant-name + // ReSharper disable once NotAccessedField.Local + private static volatile Type _dependency; + static TestHelpers() + { + _dependency = typeof(System.Data.Entity.SqlServer.SqlProviderServices); + } + public static string ReadEmbeddedFile(string path) { - var resourcePath = "JSONAPI.EntityFramework.Tests." + path.Replace("\\", ".").Replace("/", "."); + var resourcePath = "JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests." + path.Replace("\\", ".").Replace("/", "."); using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourcePath)) { if (stream == null) throw new Exception("Could not find a file at the path: " + path); diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs new file mode 100644 index 00000000..e24ae49b --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs @@ -0,0 +1,700 @@ +using System; +using System.Data.Entity; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests +{ + [TestClass] + public class UpdatingResourcesTests : AcceptanceTestsBase + { + [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 PatchWithAttributeUpdate() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithAttributeUpdateRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithAttributeUpdateResponse.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.Include(p => p.Tags).ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("New post title"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithAttributeUpdateAndInclude() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202?include=author", @"Fixtures\UpdatingResources\Requests\PatchWithAttributeUpdateRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithAttributeUpdateWithIncludeResponse.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.Include(p => p.Tags).ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("New post title"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [TestMethod] + [DeploymentItem(@"Data\PostID.csv", @"Data")] + public async Task PatchWithAttributeUpdateID() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "post-i-ds/202", @"Fixtures\UpdatingResources\Requests\PatchWithAttributeUpdateRequestID.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithAttributeUpdateResponseID.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.PostsID; + allPosts.Count().Should().Be(4); + var actualPost = allPosts.First(t => t.ID == "202"); + actualPost.ID.Should().Be("202"); + actualPost.Title.Should().Be("New post title"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + } + } + } + + [TestMethod] + [DeploymentItem(@"Data\PostLongId.csv", @"Data")] + public async Task PatchWithAttributeUpdateLongId() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "post-long-ids/202", @"Fixtures\UpdatingResources\Requests\PatchWithAttributeUpdateRequestLongId.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithAttributeUpdateResponseLongId.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.PostsLongId; + allPosts.Count().Should().Be(4); + var actualPost = allPosts.First(t => t.Id == 202); + actualPost.Id.Should().Be(202); + actualPost.Title.Should().Be("New post title"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + } + } + } + + [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 Patch_with_unknown_attribute() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\Patch_with_unknown_attribute_Request.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\Patch_with_unknown_attribute_Response.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.Include(p => p.Tags).ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("New post title"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 Patch_with_unknown_relationship() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\Patch_with_unknown_relationship_Request.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\Patch_with_unknown_relationship_Response.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.Include(p => p.Tags).ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("New post title"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithToManyUpdate() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToManyUpdateRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToManyUpdateResponse.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.Include(p => p.Tags).ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("301"); + } + } + } + + [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 PatchWithToManyHomogeneousDataUpdate() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToManyHomogeneousDataUpdateRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToManyHomogeneousDataUpdateResponse.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.Include(p => p.Tags).ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("301", "303"); + } + } + } + + [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 PatchWithToManyEmptyLinkageUpdate() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToManyEmptyLinkageUpdateRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToManyEmptyLinkageUpdateResponse.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.Include(p => p.Tags).ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Should().BeEmpty(); + } + } + } + + [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 PatchWithToOneUpdate() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToOneUpdateRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToOneUpdateResponse.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("403"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithNullToOneUpdate() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithNullToOneUpdateRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithNullToOneUpdateResponse.json", HttpStatusCode.OK); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.Include(p => p.Author).ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.Author.Should().BeNull(); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithMissingToOneLinkage() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithMissingToOneLinkageRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithMissingToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithToOneLinkageObjectMissingId() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToOneLinkageObjectMissingIdRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToOneLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest, true); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithToOneLinkageObjectMissingType() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToOneLinkageObjectMissingTypeRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToOneLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest, true); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithArrayForToOneLinkage() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithArrayForToOneLinkageRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithArrayForToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithStringForToOneLinkage() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithStringForToOneLinkageRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithStringForToOneLinkageResponse.json", HttpStatusCode.BadRequest, true); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithMissingToManyLinkage() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithMissingToManyLinkageRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithMissingToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithToManyLinkageObjectMissingId() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToManyLinkageObjectMissingIdRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToManyLinkageObjectMissingIdResponse.json", HttpStatusCode.BadRequest, true); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithToManyLinkageObjectMissingType() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithToManyLinkageObjectMissingTypeRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithToManyLinkageObjectMissingTypeResponse.json", HttpStatusCode.BadRequest, true); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithObjectForToManyLinkage() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithObjectForToManyLinkageRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithObjectForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithStringForToManyLinkage() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithStringForToManyLinkageRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithStringForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); + + using (var dbContext = new TestDbContext(effortConnection, false)) + + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithNullForToManyLinkage() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithNullForToManyLinkageRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithNullForToManyLinkageResponse.json", HttpStatusCode.BadRequest, true); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithArrayRelationshipValue() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithArrayRelationshipValueRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithArrayRelationshipValueResponse.json", HttpStatusCode.BadRequest, true); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + + [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 PatchWithStringRelationshipValue() + { + using (var effortConnection = GetEffortConnection()) + { + var response = await SubmitPatch(effortConnection, "posts/202", @"Fixtures\UpdatingResources\Requests\PatchWithStringRelationshipValueRequest.json"); + + await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithStringRelationshipValueResponse.json", HttpStatusCode.BadRequest, true); + + using (var dbContext = new TestDbContext(effortConnection, false)) + { + var allPosts = dbContext.Posts.ToArray(); + allPosts.Length.Should().Be(4); + var actualPost = allPosts.First(t => t.Id == "202"); + actualPost.Id.Should().Be("202"); + actualPost.Title.Should().Be("Post 2"); + actualPost.Content.Should().Be("Post 2 content"); + actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); + actualPost.AuthorId.Should().Be("401"); + actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303"); + } + } + } + } +} diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/packages.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/packages.config new file mode 100644 index 00000000..0243217e --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/packages.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/MainController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/MainController.cs new file mode 100644 index 00000000..6c786a1c --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/MainController.cs @@ -0,0 +1,12 @@ +using JSONAPI.Http; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Controllers +{ + public class MainController : JsonApiController + { + public MainController(IDocumentMaterializerLocator documentMaterializerLocator) + : base(documentMaterializerLocator) + { + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SamplesController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SamplesController.cs new file mode 100644 index 00000000..4d6d7546 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SamplesController.cs @@ -0,0 +1,113 @@ +using System; +using System.Web.Http; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Controllers +{ + public class SamplesController : ApiController + { + public IHttpActionResult GetSamples() + { + var s1 = new Sample + { + Id = "1", + BooleanField = false, + NullableBooleanField = false, + SbyteField = default(SByte), + NullableSbyteField = null, + ByteField = default(Byte), + NullableByteField = null, + Int16Field = default(Int16), + NullableInt16Field = null, + Uint16Field = default(UInt16), + NullableUint16Field = null, + Int32Field = default(Int32), + NullableInt32Field = null, + Uint32Field = default(Int32), + NullableUint32Field = null, + Int64Field = default(Int64), + NullableInt64Field = null, + Uint64Field = default(UInt64), + NullableUint64Field = null, + DoubleField = default(Double), + NullableDoubleField = null, + SingleField = default(Single), + NullableSingleField = null, + DecimalField = default(Decimal), + NullableDecimalField = null, + DateTimeField = default(DateTime), + NullableDateTimeField = null, + DateTimeOffsetField = default(DateTimeOffset), + NullableDateTimeOffsetField = null, + GuidField = default(Guid), + NullableGuidField = null, + StringField = null, + EnumField = default(SampleEnum), + NullableEnumField = null, + ComplexAttributeField = null, + JTokenStringField = null, + JTokenObjectField = null, + JTokenArrayField = null + }; + var s2 = new Sample + { + Id = "2", + BooleanField = true, + NullableBooleanField = true, + SbyteField = 123, + NullableSbyteField = 123, + ByteField = 253, + NullableByteField = 253, + Int16Field = 32000, + NullableInt16Field = 32000, + Uint16Field = 64000, + NullableUint16Field = 64000, + Int32Field = 2000000000, + NullableInt32Field = 2000000000, + Uint32Field = 3000000000, + NullableUint32Field = 3000000000, + Int64Field = 9223372036854775807, + NullableInt64Field = 9223372036854775807, + Uint64Field = 9223372036854775808, + NullableUint64Field = 9223372036854775808, + DoubleField = 1056789.123, + NullableDoubleField = 1056789.123, + SingleField = 1056789.123f, + NullableSingleField = 1056789.123f, + DecimalField = 1056789.123m, + NullableDecimalField = 1056789.123m, + DateTimeField = new DateTime(1776, 07, 04), + NullableDateTimeField = new DateTime(1776, 07, 04), + DateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), + NullableDateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), + GuidField = new Guid("6566F9B4-5245-40DE-890D-98B40A4AD656"), + NullableGuidField = new Guid("3D1FB81E-43EE-4D04-AF91-C8A326341293"), + StringField = "Some string 156", + EnumField = SampleEnum.Value1, + NullableEnumField = SampleEnum.Value2, + ComplexAttributeField = "{\"foo\": { \"baz\": [11] }, \"bar\": 5}", + JTokenStringField = "Andrew Jackson", + JTokenObjectField = JToken.FromObject(new SomeSerializableClass { MyField1 = "Abraham Lincoln", MyField2 = true, MyField3 = 439 }), + JTokenArrayField = new JArray( + JToken.FromObject(new SomeSerializableClass { MyField1 = "George Washington", MyField2 = null, MyField3 = 216 }), + JToken.FromObject(new SomeSerializableClass { MyField1 = "Thomas Jefferson", MyField2 = false, MyField3 = 631 })) + }; + + return Ok(new[] { s1, s2 }); + } + + [Serializable] + public class SomeSerializableClass + { + [JsonProperty("my-field1")] + public string MyField1 { get; set; } + + [JsonProperty("overridden-field2")] + public bool? MyField2 { get; set; } + + public int MyField3 { get; set; } + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SearchController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SearchController.cs new file mode 100644 index 00000000..0d4c1667 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/SearchController.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; +using System.Threading.Tasks; +using System.Web.Http; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Controllers +{ + public class SearchController : ApiController + { + private readonly TestDbContext _dbContext; + + public SearchController(TestDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task> Get(string s) + { + IEnumerable posts = await _dbContext.Posts.Where(p => p.Title.Contains(s)).ToArrayAsync(); + IEnumerable comments = await _dbContext.Comments.Where(p => p.Text.Contains(s)).ToArrayAsync(); + return posts.Concat(comments); + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/TreesController.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/TreesController.cs new file mode 100644 index 00000000..6a018db0 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Controllers/TreesController.cs @@ -0,0 +1,13 @@ +using System; +using System.Web.Http; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Controllers +{ + public class TreesController : ApiController + { + public IHttpActionResult Get() + { + throw new Exception("Something bad happened!"); + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs new file mode 100644 index 00000000..2a2c6190 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/CustomEntityFrameworkResourceObjectMaterializer.cs @@ -0,0 +1,29 @@ +using System; +using System.Data.Entity; +using System.Threading.Tasks; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using JSONAPI.Core; +using JSONAPI.Documents; +using JSONAPI.EntityFramework; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp +{ + public class CustomEntityFrameworkResourceObjectMaterializer : EntityFrameworkResourceObjectMaterializer + { + public CustomEntityFrameworkResourceObjectMaterializer(DbContext dbContext, IResourceTypeRegistry registry) : base(dbContext, registry) + { + } + + protected override Task SetIdForNewResource(IResourceObject resourceObject, object newObject, IResourceTypeRegistration typeRegistration) + { + // This is to facilitate testing creation of a resource with a server-provided ID + if (typeRegistration.Type == typeof (Post) && String.IsNullOrEmpty(resourceObject.Id)) + { + ((Post) newObject).Id = "230"; + return Task.FromResult(0); + } + + return base.SetIdForNewResource(resourceObject, newObject, typeRegistration); + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs new file mode 100644 index 00000000..9eb3fa47 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using JSONAPI.Core; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; +using JSONAPI.Http; +using JSONAPI.QueryableTransformers; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.DocumentMaterializers +{ + public class StarshipDocumentMaterializer : MappedDocumentMaterializer + { + private readonly TestDbContext _dbContext; + + public StarshipDocumentMaterializer( + TestDbContext dbContext, + IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, + IBaseUrlService baseUrlService, ISingleResourceDocumentBuilder singleResourceDocumentBuilder, + ISortExpressionExtractor sortExpressionExtractor, + IQueryableEnumerationTransformer queryableEnumerationTransformer, IResourceTypeRegistry resourceTypeRegistry) + : base( + queryableResourceCollectionDocumentBuilder, baseUrlService, singleResourceDocumentBuilder, + queryableEnumerationTransformer, sortExpressionExtractor, resourceTypeRegistry) + { + _dbContext = dbContext; + } + + protected override IQueryable GetQuery() + { + return _dbContext.Starships; + } + + protected override IQueryable GetByIdQuery(string id) + { + return GetQuery().Where(s => s.StarshipId == id); + } + + protected override IQueryable GetMappedQuery(IQueryable entityQuery, Expression>[] propertiesToInclude) + { + return entityQuery.Select(s => new StarshipDto + { + Id = s.StarshipId, + Name = s.Name, + StarshipClass = s.StarshipClass.Name + }); + } + + public override Task CreateRecord(ISingleResourceDocument requestDocument, HttpRequestMessage request, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override Task UpdateRecord(string id, ISingleResourceDocument requestDocument, HttpRequestMessage request, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override Task DeleteRecord(string id, HttpRequestMessage request, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs new file mode 100644 index 00000000..9fb646b3 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs @@ -0,0 +1,40 @@ +using System.Data.Entity; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using JSONAPI.Core; +using JSONAPI.Documents.Builders; +using JSONAPI.EntityFramework.Http; +using JSONAPI.Http; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.DocumentMaterializers +{ + public class StarshipOfficersRelatedResourceMaterializer : EntityFrameworkToManyRelatedResourceDocumentMaterializer + { + private readonly DbContext _dbContext; + + public StarshipOfficersRelatedResourceMaterializer(ResourceTypeRelationship relationship, DbContext dbContext, + IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, + ISortExpressionExtractor sortExpressionExtractor, + IIncludeExpressionExtractor includeExpressionExtractor, + IResourceTypeRegistration primaryTypeRegistration) + : base(relationship, dbContext, queryableResourceCollectionDocumentBuilder, sortExpressionExtractor, includeExpressionExtractor, primaryTypeRegistration) + { + _dbContext = dbContext; + } + + protected override Task> GetRelatedQuery(string primaryResourceId, CancellationToken cancellationToken) + { + var query = _dbContext.Set().Where(s => s.StarshipId == primaryResourceId).SelectMany(s => s.OfficerLinks) + .Select(l => new StarshipOfficerDto + { + Id = l.StarshipId + "_" + l.OfficerId, + Name = l.Officer.Name, + Rank = l.Officer.Rank, + Position = l.Position + }); + return Task.FromResult(query); + } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipShipCounselorRelatedResourceMaterializer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipShipCounselorRelatedResourceMaterializer.cs new file mode 100644 index 00000000..035c4837 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipShipCounselorRelatedResourceMaterializer.cs @@ -0,0 +1,41 @@ +using System.Data.Entity; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using JSONAPI.Core; +using JSONAPI.Documents.Builders; +using JSONAPI.EntityFramework.Http; +using JSONAPI.Http; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.DocumentMaterializers +{ + public class StarshipShipCounselorRelatedResourceMaterializer : EntityFrameworkToOneRelatedResourceDocumentMaterializer + { + private readonly DbContext _dbContext; + + public StarshipShipCounselorRelatedResourceMaterializer( + ISingleResourceDocumentBuilder singleResourceDocumentBuilder, IBaseUrlService baseUrlService, + IResourceTypeRegistration primaryTypeRegistration, ResourceTypeRelationship relationship, + DbContext dbContext) + : base(singleResourceDocumentBuilder, baseUrlService, primaryTypeRegistration, relationship, dbContext) + { + _dbContext = dbContext; + } + + protected override async Task GetRelatedRecord(string primaryResourceId, CancellationToken cancellationToken) + { + var query = _dbContext.Set().Where(s => s.StarshipId == primaryResourceId) + .SelectMany(s => s.OfficerLinks) + .Where(l => l.Position == "Ship's Counselor") + .Select(l => new StarshipOfficerDto + { + Id = l.StarshipId + "_" + l.OfficerId, + Name = l.Officer.Name, + Rank = l.Officer.Rank, + Position = l.Position + }); + return await query.FirstOrDefaultAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj similarity index 70% rename from JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj index b4e0f039..0a5148ea 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/JSONAPI.EntityFramework.Tests.TestWebApp.csproj +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.csproj @@ -1,179 +1,216 @@ - - - - - Debug - AnyCPU - - - 2.0 - {76DEE472-723B-4BE6-8B97-428AC326E30F} - {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} - Library - Properties - JSONAPI.EntityFramework.Tests.TestWebApp - JSONAPI.EntityFramework.Tests.TestWebApp - v4.5 - true - - - - - ..\ - true - - - true - full - false - bin\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\ - TRACE - prompt - 4 - - - - ..\packages\Autofac.3.5.2\lib\net40\Autofac.dll - - - ..\packages\Autofac.Owin.3.1.0\lib\net45\Autofac.Integration.Owin.dll - - - ..\packages\Autofac.WebApi2.3.4.0\lib\net45\Autofac.Integration.WebApi.dll - - - ..\packages\Autofac.WebApi2.Owin.3.2.0\lib\net45\Autofac.Integration.WebApi.Owin.dll - - - False - ..\packages\EntityFramework.6.1.1\lib\net45\EntityFramework.dll - - - False - ..\packages\EntityFramework.6.1.1\lib\net45\EntityFramework.SqlServer.dll - - - - ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll - - - ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll - - - ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll - - - ..\packages\Owin.1.0\lib\net40\Owin.dll - - - - False - ..\packages\Microsoft.AspNet.WebApi.Client.5.2.2\lib\net45\System.Net.Http.Formatting.dll - - - - - - - - - - - - False - ..\packages\Microsoft.AspNet.WebApi.Core.5.2.2\lib\net45\System.Web.Http.dll - - - ..\packages\Microsoft.AspNet.WebApi.Owin.5.2.2\lib\net45\System.Web.Http.Owin.dll - - - - - - - - - - - - - Web.config - - - Web.config - - - - - - - - - - - - - - - - - - - - - {e906356c-93f6-41f6-9a0d-73b8a99aa53c} - JSONAPI.EntityFramework - - - {52b19fd6-efaa-45b5-9c3e-a652e27608d1} - JSONAPI - - - - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - - - - - True - True - 9283 - / - http://localhost:9283/ - False - False - - - False - - - - - - - - This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - + + + + + Debug + AnyCPU + + + 2.0 + {76DEE472-723B-4BE6-8B97-428AC326E30F} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp + JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp + v4.5 + true + + + + + ..\ + true + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\ + TRACE + prompt + 4 + + + + False + ..\packages\Autofac.3.5.2\lib\net40\Autofac.dll + + + False + ..\packages\Autofac.Owin.3.1.0\lib\net45\Autofac.Integration.Owin.dll + + + False + ..\packages\Autofac.WebApi2.3.4.0\lib\net45\Autofac.Integration.WebApi.dll + + + False + ..\packages\Autofac.WebApi2.Owin.3.2.0\lib\net45\Autofac.Integration.WebApi.Owin.dll + + + False + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + + + False + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + + + + ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + + + ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + + + False + ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + + + ..\packages\Owin.1.0\lib\net40\Owin.dll + + + + False + ..\packages\Microsoft.AspNet.WebApi.Client.5.2.2\lib\net45\System.Net.Http.Formatting.dll + + + + + + + + + + + + False + ..\packages\Microsoft.AspNet.WebApi.Core.5.2.2\lib\net45\System.Web.Http.dll + + + ..\packages\Microsoft.AspNet.WebApi.Owin.5.2.2\lib\net45\System.Web.Http.Owin.dll + + + + + + + + + + + + + Web.config + + + Web.config + + + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {64abe648-efcb-46ee-9e1a-e163f52bf372} + JSONAPI.Autofac.EntityFramework + + + {af7861f3-550b-4f70-a33e-1e5f48d39333} + JSONAPI.Autofac + + + {e906356c-93f6-41f6-9a0d-73b8a99aa53c} + JSONAPI.EntityFramework + + + {52b19fd6-efaa-45b5-9c3e-a652e27608d1} + JSONAPI + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + + True + True + 9283 + / + http://localhost:9283/ + False + False + + + False + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Building.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Building.cs new file mode 100644 index 00000000..3bfd590e --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Building.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class Building + { + [Key] + public string Id { get; set; } + + public string Address { get; set; } + + [JsonIgnore] + public string OwnerCompanyId { get; set; } + + [ForeignKey("OwnerCompanyId")] + public virtual Company Owner { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Child.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Child.cs new file mode 100644 index 00000000..b3aefcc7 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Child.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class Child + { + public int Id { get; set; } + + public string ChildDescription { get; set; } + + [Required] + [JsonIgnore] + public int MasterId { get; set; } + + [ForeignKey("MasterId")] + public Master Master { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/City.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/City.cs new file mode 100644 index 00000000..43283f17 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/City.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using JSONAPI.Attributes; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class City + { + [Key] + public string Id { get; set; } + + public string Name { get; set; } + + [RelatedResourceLinkTemplate("/cities/{1}/state")] + public virtual State State { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Comment.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Comment.cs similarity index 90% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/Comment.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Comment.cs index 31d70a7f..f031485a 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Comment.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Comment.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class Comment { diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Company.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Company.cs new file mode 100644 index 00000000..55a4d6e7 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Company.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class Company + { + [Key] + public string Id { get; set; } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Language.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Language.cs new file mode 100644 index 00000000..4a8ec476 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Language.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class Language + { + [Key] + public string Id { get; set; } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/LanguageUserLink.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/LanguageUserLink.cs new file mode 100644 index 00000000..8a895ea6 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/LanguageUserLink.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class LanguageUserLink + { + public string Id { get { return LanguageId + "_" + UserId; } } + + [JsonIgnore] + [Key, Column(Order = 1)] + public string LanguageId { get; set; } + + [JsonIgnore] + [Key, Column(Order = 2)] + public string UserId { get; set; } + + public string FluencyLevel { get; set; } + + [ForeignKey("LanguageId")] + public virtual Language Language { get; set; } + + [ForeignKey("UserId")] + public virtual User User { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Master.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Master.cs new file mode 100644 index 00000000..5ad2015c --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Master.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class Master + { + public int Id { get; set; } + + public string Description { get; set; } + + public virtual ICollection Children { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Officer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Officer.cs new file mode 100644 index 00000000..e167d4be --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Officer.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class Officer + { + [Key] + public string OfficerId { get; set; } + + public string Name { get; set; } + + public string Rank { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Post.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Post.cs similarity index 89% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/Post.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Post.cs index dbcb4363..5c750537 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Post.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Post.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class Post { @@ -17,7 +17,6 @@ public class Post public DateTimeOffset Created { get; set; } - [Required] [JsonIgnore] public string AuthorId { get; set; } diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/PostID.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/PostID.cs new file mode 100644 index 00000000..4e7bd60f --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/PostID.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class PostID + { + [Key] + public string ID { get; set; } + + public string Title { get; set; } + + public string Content { get; set; } + + public DateTimeOffset Created { get; set; } + + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/PostLongId.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/PostLongId.cs new file mode 100644 index 00000000..24e830d5 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/PostLongId.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class PostLongId + { + [Key] + public long Id { get; set; } + + public string Title { get; set; } + + public string Content { get; set; } + + public DateTimeOffset Created { get; set; } + + } +} \ No newline at end of file diff --git a/JSONAPI.Tests/Models/Sample.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Sample.cs similarity index 63% rename from JSONAPI.Tests/Models/Sample.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Sample.cs index 49342393..e8096599 100644 --- a/JSONAPI.Tests/Models/Sample.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Sample.cs @@ -1,34 +1,36 @@ using System; +using JSONAPI.Attributes; +using Newtonsoft.Json.Linq; -namespace JSONAPI.Tests.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { - enum SampleEnum + public enum SampleEnum { Value1 = 1, Value2 = 2 } - class Sample + public class Sample { public string Id { get; set; } public Boolean BooleanField { get; set; } public Boolean? NullableBooleanField { get; set; } - public SByte SByteField { get; set; } - public SByte? NullableSByteField { get; set; } + public SByte SbyteField { get; set; } + public SByte? NullableSbyteField { get; set; } public Byte ByteField { get; set; } public Byte? NullableByteField { get; set; } public Int16 Int16Field { get; set; } public Int16? NullableInt16Field { get; set; } - public UInt16 UInt16Field { get; set; } - public UInt16? NullableUInt16Field { get; set; } + public UInt16 Uint16Field { get; set; } + public UInt16? NullableUint16Field { get; set; } public Int32 Int32Field { get; set; } public Int32? NullableInt32Field { get; set; } - public UInt32 UInt32Field { get; set; } - public UInt32? NullableUInt32Field { get; set; } + public UInt32 Uint32Field { get; set; } + public UInt32? NullableUint32Field { get; set; } public Int64 Int64Field { get; set; } public Int64? NullableInt64Field { get; set; } - public UInt64 UInt64Field { get; set; } - public UInt64? NullableUInt64Field { get; set; } + public UInt64 Uint64Field { get; set; } + public UInt64? NullableUint64Field { get; set; } public Double DoubleField { get; set; } public Double? NullableDoubleField { get; set; } public Single SingleField { get; set; } @@ -44,5 +46,14 @@ class Sample public string StringField { get; set; } public SampleEnum EnumField { get; set; } public SampleEnum? NullableEnumField { get; set; } + + [SerializeAsComplex] + public string ComplexAttributeField { get; set; } + + public JToken JTokenStringField { get; set; } + + public JToken JTokenObjectField { get; set; } + + public JToken JTokenArrayField { get; set; } } } diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Starship.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Starship.cs new file mode 100644 index 00000000..96a55e60 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Starship.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class Starship + { + [Key] + public string StarshipId { get; set; } + + public string Name { get; set; } + + public string StarshipClassId { get; set; } + + [ForeignKey("StarshipClassId")] + public virtual StarshipClass StarshipClass { get; set; } + + public virtual ICollection OfficerLinks { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipClass.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipClass.cs new file mode 100644 index 00000000..028180c6 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipClass.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class StarshipClass + { + [Key] + public string StarshipClassId { get; set; } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipDto.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipDto.cs new file mode 100644 index 00000000..e081fe44 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipDto.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + [JsonObject(Title = "starship")] + public class StarshipDto + { + public string Id { get; set; } + + public string Name { get; set; } + + public string StarshipClass { get; set; } + + public virtual ICollection Officers { get; set; } + + public virtual StarshipOfficerDto ShipCounselor { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipOfficerDto.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipOfficerDto.cs new file mode 100644 index 00000000..ca18f6db --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipOfficerDto.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + [JsonObject(Title = "starship-officer")] + public class StarshipOfficerDto + { + public string Id { get; set; } + + public string Name { get; set; } + + public string Rank { get; set; } + + public string Position { get; set; } + + public virtual StarshipDto CurrentShip { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipOfficerLink.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipOfficerLink.cs new file mode 100644 index 00000000..26b5704b --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/StarshipOfficerLink.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class StarshipOfficerLink + { + [Key, Column(Order = 0)] + public string StarshipId { get; set; } + + [Key, Column(Order = 1)] + public string OfficerId { get; set; } + + [ForeignKey("StarshipId")] + public virtual Starship Starship { get; set; } + + [ForeignKey("OfficerId")] + public virtual Officer Officer { get; set; } + + public string Position { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/State.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/State.cs new file mode 100644 index 00000000..3b7ed57f --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/State.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class State + { + [Key] + public string Id { get; set; } + + public string Name { get; set; } + + public virtual ICollection Cities { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Tag.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Tag.cs similarity index 80% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/Tag.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Tag.cs index b8529429..9d9f25b6 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/Tag.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/Tag.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class Tag { diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/TestDbContext.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs similarity index 60% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/TestDbContext.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs index af3c02a2..4850d0a0 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/TestDbContext.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/TestDbContext.cs @@ -2,7 +2,7 @@ using System.Data.Entity; using System.Data.Entity.ModelConfiguration.Conventions; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class TestDbContext : DbContext { @@ -35,9 +35,21 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder) .Map(c => c.MapLeftKey("PostId").MapRightKey("TagId").ToTable("PostTagLink")); } + public DbSet Buildings { get; set; } + public DbSet Companies { get; set; } public DbSet Comments { get; set; } + public DbSet Languages { get; set; } public DbSet Posts { get; set; } + public DbSet PostsID { get; set; } + public DbSet PostsLongId { get; set; } public DbSet Tags { get; set; } public DbSet Users { get; set; } + public DbSet LanguageUserLinks { get; set; } + public DbSet Starships { get; set; } + public DbSet StarshipClasses { get; set; } + public DbSet Officers { get; set; } + public DbSet StarshipOfficerLinks { get; set; } + public DbSet Masters { get; set; } + public DbSet Children { get; set; } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/User.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/User.cs similarity index 56% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Models/User.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/User.cs index 5c80a9f1..54596d11 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Models/User.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/User.cs @@ -1,17 +1,21 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Models +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models { public class User { [Key] public string Id { get; set; } - public string Name { get; set; } + public string FirstName { get; set; } + + public string LastName { get; set; } public virtual ICollection Comments { get; set; } public virtual ICollection Posts { get; set; } + + public virtual ICollection UserGroups { get; set; } } } \ No newline at end of file diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/UserGroup.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/UserGroup.cs new file mode 100644 index 00000000..fd154f87 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Models/UserGroup.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models +{ + public class UserGroup + { + [Key] + public string Id { get; set; } + + public string Name { get; set; } + + public virtual ICollection Users { get; set; } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Properties/AssemblyInfo.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs similarity index 94% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Properties/AssemblyInfo.cs rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs index ababced3..130abbb8 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Properties/AssemblyInfo.cs +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Properties/AssemblyInfo.cs @@ -33,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 new file mode 100644 index 00000000..678f8532 --- /dev/null +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs @@ -0,0 +1,171 @@ +using System; +using System.Data.Entity; +using System.Linq.Expressions; +using System.Reflection; +using System.Web.Http; +using Autofac; +using Autofac.Integration.WebApi; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.DocumentMaterializers; +using JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Models; +using JSONAPI.Autofac; +using JSONAPI.Autofac.EntityFramework; +using JSONAPI.Configuration; +using JSONAPI.EntityFramework; +using JSONAPI.EntityFramework.Configuration; +using Owin; +using System.Collections.Generic; +using JSONAPI.Documents.Builders; +using JSONAPI.EntityFramework.Documents.Builders; + +namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp +{ + public class Startup + { + private readonly Func _dbContextFactory; + + public Startup() + : this(() => new TestDbContext()) + { + + } + + public Startup(Func dbContextFactory) + { + _dbContextFactory = 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 Dictionary { { "Child", "Children" } })); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(c => + { + c.OverrideDefaultFilterById(LanguageUserLinkFilterByIdFactory); + c.OverrideDefaultSortById(LanguageUserLinkSortByIdFactory); + }); + configuration.RegisterResourceType(); + configuration.RegisterResourceType(); + configuration.RegisterResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterEntityFrameworkResourceType(); + configuration.RegisterResourceType(); // Example of a resource type not controlled by EF + configuration.RegisterMappedType(c => + { + c.ConfigureRelationship(s => s.Officers, + rc => rc.UseMaterializer()); + c.ConfigureRelationship(s => s.ShipCounselor, + rc => rc.UseMaterializer()); + }); // Example of a resource that is mapped from a DB entity + 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 => + { + builder.Register(c => _dbContextFactory()) + .AsSelf() + .As() + .InstancePerRequest(); + builder.RegisterModule(); + builder.RegisterType() + .As(); + builder.RegisterApiControllers(Assembly.GetExecutingAssembly()); + builder.RegisterType().As(); + + }); + configurator.OnApplicationLifetimeScopeBegun(applicationLifetimeScope => + { + // 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 + }; + + // Additional Web API routes + httpConfig.Routes.MapHttpRoute("Samples", "samples", new { Controller = "Samples" }); + httpConfig.Routes.MapHttpRoute("Search", "search", new { Controller = "Search" }); + httpConfig.Routes.MapHttpRoute("Trees", "trees", new { Controller = "Trees" }); + return httpConfig; + } + + + private BinaryExpression LanguageUserLinkFilterByIdFactory(ParameterExpression param, string id) + { + var split = id.Split('_'); + var languageId = Expression.Constant(split[0]); + var userId = Expression.Constant(split[1]); + + var languageIdPropertyExpr = Expression.Property(param, "LanguageId"); + var languageIdPropertyEqualsExpr = Expression.Equal(languageIdPropertyExpr, languageId); + + var userIdPropertyExpr = Expression.Property(param, "UserId"); + var userIdPropertyEqualsExpr = Expression.Equal(userIdPropertyExpr, userId); + + return Expression.AndAlso(languageIdPropertyEqualsExpr, userIdPropertyEqualsExpr); + } + + private Expression LanguageUserLinkSortByIdFactory(ParameterExpression param) + { + var concatMethod = typeof(string).GetMethod("Concat", new[] { typeof(object), typeof(object) }); + + var languageIdExpr = Expression.Property(param, "LanguageId"); + var userIdExpr = Expression.Property(param, "UserId"); + return Expression.Call(concatMethod, languageIdExpr, userIdExpr); + } + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Web.Debug.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.Debug.config similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Web.Debug.config rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.Debug.config diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Web.Release.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.Release.config similarity index 100% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Web.Release.config rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.Release.config diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Web.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.config similarity index 98% rename from JSONAPI.EntityFramework.Tests.TestWebApp/Web.config rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.config index 1bc02ea7..2d263f08 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Web.config +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Web.config @@ -5,7 +5,7 @@ --> - +
@@ -22,7 +22,7 @@ - + diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/packages.config b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/packages.config similarity index 87% rename from JSONAPI.EntityFramework.Tests.TestWebApp/packages.config rename to JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/packages.config index 116d5bd9..bba30ec4 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/packages.config +++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/packages.config @@ -4,12 +4,12 @@ - + - + \ No newline at end of file diff --git a/JSONAPI.Autofac.EntityFramework/JSONAPI.Autofac.EntityFramework.csproj b/JSONAPI.Autofac.EntityFramework/JSONAPI.Autofac.EntityFramework.csproj new file mode 100644 index 00000000..b7ee5706 --- /dev/null +++ b/JSONAPI.Autofac.EntityFramework/JSONAPI.Autofac.EntityFramework.csproj @@ -0,0 +1,84 @@ + + + + + Debug + AnyCPU + {64ABE648-EFCB-46EE-9E1A-E163F52BF372} + Library + Properties + JSONAPI.Autofac.EntityFramework + JSONAPI.Autofac.EntityFramework + v4.5 + 512 + ..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\packages\Autofac.3.5.2\lib\net40\Autofac.dll + + + + + + + + + + + + + + + + + + + + {af7861f3-550b-4f70-a33e-1e5f48d39333} + JSONAPI.Autofac + + + {e906356c-93f6-41f6-9a0d-73b8a99aa53c} + JSONAPI.EntityFramework + + + {52b19fd6-efaa-45b5-9c3e-a652e27608d1} + JSONAPI + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs b/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs new file mode 100644 index 00000000..481e823e --- /dev/null +++ b/JSONAPI.Autofac.EntityFramework/JsonApiAutofacEntityFrameworkModule.cs @@ -0,0 +1,79 @@ +using System; +using System.Linq; +using Autofac; +using JSONAPI.Core; +using JSONAPI.EntityFramework; +using JSONAPI.EntityFramework.ActionFilters; +using JSONAPI.EntityFramework.Http; +using JSONAPI.Http; +using JSONAPI.QueryableTransformers; + +namespace JSONAPI.Autofac.EntityFramework +{ + public class JsonApiAutofacEntityFrameworkModule : Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().As(); + builder.RegisterType() + .As(); + + builder.RegisterGeneric(typeof (EntityFrameworkDocumentMaterializer<>)); + builder.Register((ctx, parameters) => + { + var allParameters = parameters.ToArray(); + var typedParameters = allParameters.OfType().ToArray(); + var resourceTypeRegistrationParameter = + typedParameters.FirstOrDefault(tp => tp.Type == typeof(IResourceTypeRegistration)); + if (resourceTypeRegistrationParameter == null) + throw new Exception( + "An IResourceTypeRegistration parameter must be provided to resolve an instance of EntityFrameworkDocumentMaterializer."); + + var resourceTypeRegistration = resourceTypeRegistrationParameter.Value as IResourceTypeRegistration; + if (resourceTypeRegistration == null) + throw new Exception( + "An IResourceTypeRegistration parameter was provided to resolve EntityFrameworkDocumentMaterializer, but its value was null."); + + var openGenericType = typeof (EntityFrameworkDocumentMaterializer<>); + var materializerType = openGenericType.MakeGenericType(resourceTypeRegistration.Type); + return ctx.Resolve(materializerType, allParameters); + }).As(); + + builder.RegisterGeneric(typeof(EntityFrameworkToManyRelatedResourceDocumentMaterializer<,>)); + builder.RegisterGeneric(typeof(EntityFrameworkToOneRelatedResourceDocumentMaterializer<,>)); + builder.Register((ctx, parameters) => + { + var allParameters = parameters.ToArray(); + var typedParameters = allParameters.OfType().ToArray(); + var resourceTypeRegistrationParameter = + typedParameters.FirstOrDefault(tp => tp.Type == typeof(IResourceTypeRegistration)); + if (resourceTypeRegistrationParameter == null) + throw new Exception( + "An IResourceTypeRegistration parameter must be provided to resolve an instance of EntityFrameworkDocumentMaterializer."); + + var resourceTypeRegistration = resourceTypeRegistrationParameter.Value as IResourceTypeRegistration; + if (resourceTypeRegistration == null) + throw new Exception( + "An IResourceTypeRegistration parameter was provided to resolve EntityFrameworkDocumentMaterializer, but its value was null."); + + var resourceTypeRelationshipParameter = + typedParameters.FirstOrDefault(tp => tp.Type == typeof(ResourceTypeRelationship)); + if (resourceTypeRelationshipParameter == null) + throw new Exception( + "A ResourceTypeRelationship parameter must be provided to resolve an instance of EntityFrameworkDocumentMaterializer."); + + var resourceTypeRelationship = resourceTypeRelationshipParameter.Value as ResourceTypeRelationship; + if (resourceTypeRelationship == null) + throw new Exception( + "A ResourceTypeRelationship parameter was provided to resolve EntityFrameworkDocumentMaterializer, but its value was null."); + + var openGenericType = resourceTypeRelationship.IsToMany + ? typeof (EntityFrameworkToManyRelatedResourceDocumentMaterializer<,>) + : typeof (EntityFrameworkToOneRelatedResourceDocumentMaterializer<,>); + var materializerType = openGenericType.MakeGenericType(resourceTypeRegistration.Type, + resourceTypeRelationship.RelatedType); + return ctx.Resolve(materializerType, allParameters); + }).As(); + } + } +} diff --git a/JSONAPI.Autofac.EntityFramework/Properties/AssemblyInfo.cs b/JSONAPI.Autofac.EntityFramework/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..a30c9bd7 --- /dev/null +++ b/JSONAPI.Autofac.EntityFramework/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("JSONAPI.Autofac.EntityFramework")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("JSONAPI.Autofac.EntityFramework")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5c85a230-f640-43cf-ae30-b685d237a6dc")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/JSONAPI.Autofac.EntityFramework/app.config b/JSONAPI.Autofac.EntityFramework/app.config new file mode 100644 index 00000000..b678ca2c --- /dev/null +++ b/JSONAPI.Autofac.EntityFramework/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/JSONAPI.Autofac.EntityFramework/packages.config b/JSONAPI.Autofac.EntityFramework/packages.config new file mode 100644 index 00000000..f9427518 --- /dev/null +++ b/JSONAPI.Autofac.EntityFramework/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/JSONAPI.Autofac.Tests/JSONAPI.Autofac.Tests.csproj b/JSONAPI.Autofac.Tests/JSONAPI.Autofac.Tests.csproj new file mode 100644 index 00000000..8d82db0c --- /dev/null +++ b/JSONAPI.Autofac.Tests/JSONAPI.Autofac.Tests.csproj @@ -0,0 +1,89 @@ + + + + Debug + AnyCPU + {AEA3C57B-8360-42FF-95E8-0F3A6BA5BCB1} + Library + Properties + JSONAPI.Autofac.Tests + JSONAPI.Autofac.Tests + v4.5 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + {af7861f3-550b-4f70-a33e-1e5f48d39333} + JSONAPI.Autofac + + + + + + + False + + + False + + + False + + + False + + + + + + + + \ No newline at end of file diff --git a/JSONAPI.Autofac.Tests/Properties/AssemblyInfo.cs b/JSONAPI.Autofac.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..e8bddb2b --- /dev/null +++ b/JSONAPI.Autofac.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("JSONAPI.Autofac.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("JSONAPI.Autofac.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5755d01a-25ed-4be5-9f24-c9a659699558")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/JSONAPI.Autofac.Tests/UnitTest1.cs b/JSONAPI.Autofac.Tests/UnitTest1.cs new file mode 100644 index 00000000..d7da0c41 --- /dev/null +++ b/JSONAPI.Autofac.Tests/UnitTest1.cs @@ -0,0 +1,14 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Autofac.Tests +{ + [TestClass] + public class UnitTest1 + { + [TestMethod] + public void TestMethod1() + { + } + } +} diff --git a/JSONAPI.Autofac/JSONAPI.Autofac.csproj b/JSONAPI.Autofac/JSONAPI.Autofac.csproj new file mode 100644 index 00000000..d9dcafba --- /dev/null +++ b/JSONAPI.Autofac/JSONAPI.Autofac.csproj @@ -0,0 +1,95 @@ + + + + + Debug + AnyCPU + {AF7861F3-550B-4F70-A33E-1E5F48D39333} + Library + Properties + JSONAPI.Autofac + JSONAPI.Autofac + v4.5 + 512 + ..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\packages\Autofac.3.5.2\lib\net40\Autofac.dll + + + False + ..\packages\Autofac.WebApi2.3.4.0\lib\net45\Autofac.Integration.WebApi.dll + + + False + ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + + + + + + False + ..\packages\Microsoft.AspNet.WebApi.Client.5.2.2\lib\net45\System.Net.Http.Formatting.dll + + + False + ..\packages\Microsoft.AspNet.WebApi.Core.5.2.2\lib\net45\System.Web.Http.dll + + + + + + + + + + + + + + + + + + + + {52b19fd6-efaa-45b5-9c3e-a652e27608d1} + JSONAPI + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/JSONAPI.Autofac/JsonApiAutofacModule.cs b/JSONAPI.Autofac/JsonApiAutofacModule.cs new file mode 100644 index 00000000..dd248aa8 --- /dev/null +++ b/JSONAPI.Autofac/JsonApiAutofacModule.cs @@ -0,0 +1,172 @@ +using System; +using Autofac; +using Autofac.Core; +using JSONAPI.ActionFilters; +using JSONAPI.Configuration; +using JSONAPI.Core; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; +using JSONAPI.Http; +using JSONAPI.Json; +using JSONAPI.QueryableTransformers; + +namespace JSONAPI.Autofac +{ + public class JsonApiAutofacModule : Module + { + private readonly IJsonApiConfiguration _jsonApiConfiguration; + + public JsonApiAutofacModule(IJsonApiConfiguration jsonApiConfiguration) + { + _jsonApiConfiguration = jsonApiConfiguration; + } + + protected override void Load(ContainerBuilder builder) + { + // Register resource types + var registry = new ResourceTypeRegistry(); + foreach (var resourceTypeConfiguration in _jsonApiConfiguration.ResourceTypeConfigurations) + { + var resourceTypeRegistration = resourceTypeConfiguration.BuildResourceTypeRegistration(); + registry.AddRegistration(resourceTypeRegistration); + + var configuration = resourceTypeConfiguration; + builder.Register(c => configuration) + .Keyed(resourceTypeRegistration.Type) + .Keyed(resourceTypeRegistration.ResourceTypeName) + .SingleInstance(); + + if (resourceTypeConfiguration.DocumentMaterializerType != null) + builder.RegisterType(resourceTypeConfiguration.DocumentMaterializerType); + + foreach (var relationship in resourceTypeRegistration.Relationships) + { + IResourceTypeRelationshipConfiguration relationshipConfiguration; + if (resourceTypeConfiguration.RelationshipConfigurations + .TryGetValue(relationship.Property.Name, out relationshipConfiguration)) + { + if (relationshipConfiguration.MaterializerType != null) + { + builder.RegisterType(relationshipConfiguration.MaterializerType); + continue; + } + } + + // They didn't set an explicit materializer. See if they specified a factory for this resource type. + if (configuration.RelatedResourceMaterializerTypeFactory == null) continue; + + var materializerType = configuration.RelatedResourceMaterializerTypeFactory(relationship); + builder.RegisterType(materializerType); + } + } + + builder.Register(c => registry).As().SingleInstance(); + builder.Register(c => + { + var context = c.Resolve(); + Func factory = resourceTypeName => + { + var configuration = context.ResolveKeyed(resourceTypeName); + var registration = registry.GetRegistrationForResourceTypeName(resourceTypeName); + var parameters = new Parameter[] { new TypedParameter(typeof (IResourceTypeRegistration), registration) }; + if (configuration.DocumentMaterializerType != null) + return (IDocumentMaterializer)context.Resolve(configuration.DocumentMaterializerType, parameters); + return context.Resolve(parameters); + }; + return factory; + }); + builder.Register(c => + { + var context = c.Resolve(); + Func factory = clrType => + { + var configuration = context.ResolveKeyed(clrType); + var registration = registry.GetRegistrationForType(clrType); + var parameters = new Parameter[] { new TypedParameter(typeof(IResourceTypeRegistration), registration) }; + if (configuration.DocumentMaterializerType != null) + return (IDocumentMaterializer)context.Resolve(configuration.DocumentMaterializerType, parameters); + return context.Resolve(parameters); + }; + return factory; + }); + builder.Register(c => + { + var context = c.Resolve(); + Func factory = (resourceTypeName, relationshipName) => + { + var configuration = context.ResolveKeyed(resourceTypeName); + var registration = registry.GetRegistrationForResourceTypeName(resourceTypeName); + var relationship = registration.GetFieldByName(relationshipName) as ResourceTypeRelationship; + if (relationship == null) + throw JsonApiException.CreateForNotFound( + string.Format("No relationship `{0}` exists for the resource type `{1}`.", relationshipName, resourceTypeName)); + + var parameters = new Parameter[] + { + new TypedParameter(typeof(IResourceTypeRegistration), registration), + new TypedParameter(typeof(ResourceTypeRelationship), relationship) + }; + + // First, see if they have set an explicit materializer for this relationship + IResourceTypeRelationshipConfiguration relationshipConfiguration; + if (configuration.RelationshipConfigurations.TryGetValue(relationship.Property.Name, + out relationshipConfiguration) && relationshipConfiguration.MaterializerType != null) + return (IRelatedResourceDocumentMaterializer)context.Resolve(relationshipConfiguration.MaterializerType, parameters); + + // They didn't set an explicit materializer. See if they specified a factory for this resource type. + if (configuration.RelatedResourceMaterializerTypeFactory != null) + { + var materializerType = configuration.RelatedResourceMaterializerTypeFactory(relationship); + return (IRelatedResourceDocumentMaterializer)context.Resolve(materializerType, parameters); + } + + return context.Resolve(parameters); + }; + return factory; + }); + + builder.RegisterType().SingleInstance(); + if (_jsonApiConfiguration.CustomBaseUrlService != null) + { + builder.Register(c => _jsonApiConfiguration.CustomBaseUrlService).As().SingleInstance(); + } + else + { + builder.RegisterType().As().SingleInstance(); + } + builder.RegisterType().As().InstancePerRequest(); + + // Serialization + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + + // Queryable transforms + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + + // Document building + builder.Register(c => _jsonApiConfiguration.LinkConventions).As().SingleInstance(); + builder.RegisterType().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().SingleInstance(); + builder.RegisterType().SingleInstance(); + builder.RegisterType().As(); + + // Misc + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + } + } +} diff --git a/JSONAPI.Autofac/JsonApiConfigurationExtensions.cs b/JSONAPI.Autofac/JsonApiConfigurationExtensions.cs new file mode 100644 index 00000000..56622b0c --- /dev/null +++ b/JSONAPI.Autofac/JsonApiConfigurationExtensions.cs @@ -0,0 +1,17 @@ +using System.Web.Http; +using Autofac; +using Autofac.Integration.WebApi; +using JSONAPI.Configuration; + +namespace JSONAPI.Autofac +{ + public static class JsonApiConfigurationExtensions + { + public static void SetupHttpConfigurationUsingAutofac(this IJsonApiConfiguration configuration, + HttpConfiguration httpConfiguration, ILifetimeScope parentLifetimeScope) + { + var configurator = new JsonApiHttpAutofacConfigurator(parentLifetimeScope); + configurator.Apply(httpConfiguration, configuration); + } + } +} diff --git a/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs b/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs new file mode 100644 index 00000000..035f8c09 --- /dev/null +++ b/JSONAPI.Autofac/JsonApiHttpAutofacConfigurator.cs @@ -0,0 +1,68 @@ +using System; +using System.Web.Http; +using Autofac; +using Autofac.Integration.WebApi; +using JSONAPI.Configuration; + +namespace JSONAPI.Autofac +{ + public class JsonApiHttpAutofacConfigurator + { + private readonly ILifetimeScope _lifetimeScope; + private Action _appLifetimeScopeCreating; + private Action _appLifetimeScopeBegunAction; + + public JsonApiHttpAutofacConfigurator() + { + } + + public JsonApiHttpAutofacConfigurator(ILifetimeScope lifetimeScope) + { + _lifetimeScope = lifetimeScope; + } + + public void OnApplicationLifetimeScopeCreating(Action appLifetimeScopeCreating) + { + _appLifetimeScopeCreating = appLifetimeScopeCreating; + } + + public void OnApplicationLifetimeScopeBegun(Action appLifetimeScopeBegunAction) + { + _appLifetimeScopeBegunAction = appLifetimeScopeBegunAction; + } + + public void Apply(HttpConfiguration httpConfiguration, IJsonApiConfiguration jsonApiConfiguration) + { + ILifetimeScope applicationLifetimeScope; + if (_lifetimeScope == null) + { + var builder = new ContainerBuilder(); + ConfigureApplicationLifetimeScope(jsonApiConfiguration, builder); + applicationLifetimeScope = builder.Build(); + } + else + { + applicationLifetimeScope = _lifetimeScope.BeginLifetimeScope(containerBuilder => + { + ConfigureApplicationLifetimeScope(jsonApiConfiguration, containerBuilder); + }); + } + + if (_appLifetimeScopeBegunAction != null) + _appLifetimeScopeBegunAction(applicationLifetimeScope); + + var jsonApiHttpConfiguration = applicationLifetimeScope.Resolve(); + jsonApiHttpConfiguration.Apply(httpConfiguration, jsonApiConfiguration); + httpConfiguration.DependencyResolver = new AutofacWebApiDependencyResolver(applicationLifetimeScope); + } + + private void ConfigureApplicationLifetimeScope(IJsonApiConfiguration jsonApiConfiguration, ContainerBuilder containerBuilder) + { + var module = new JsonApiAutofacModule(jsonApiConfiguration); + containerBuilder.RegisterModule(module); + + if (_appLifetimeScopeCreating != null) + _appLifetimeScopeCreating(containerBuilder); + } + } +} diff --git a/JSONAPI.Autofac/Properties/AssemblyInfo.cs b/JSONAPI.Autofac/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..9804046c --- /dev/null +++ b/JSONAPI.Autofac/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("JSONAPI.Autofac")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("JSONAPI.Autofac")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("868ac502-6972-4889-bcbd-9d6160258667")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/JSONAPI.Autofac/app.config b/JSONAPI.Autofac/app.config new file mode 100644 index 00000000..b678ca2c --- /dev/null +++ b/JSONAPI.Autofac/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/JSONAPI.Autofac/packages.config b/JSONAPI.Autofac/packages.config new file mode 100644 index 00000000..2ad7611f --- /dev/null +++ b/JSONAPI.Autofac/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs deleted file mode 100644 index 73fe5d25..00000000 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/CommentsController.cs +++ /dev/null @@ -1,21 +0,0 @@ -using JSONAPI.Core; -using JSONAPI.EntityFramework.Http; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; - -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers -{ - public class CommentsController : ApiController - { - protected readonly TestDbContext DbContext; - - public CommentsController(TestDbContext dbContext) - { - DbContext = dbContext; - } - - protected override IMaterializer MaterializerFactory() - { - return new EntityFrameworkMaterializer(DbContext); - } - } -} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs deleted file mode 100644 index 0d368070..00000000 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/PostsController.cs +++ /dev/null @@ -1,21 +0,0 @@ -using JSONAPI.Core; -using JSONAPI.EntityFramework.Http; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; - -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers -{ - public class PostsController : ApiController - { - protected readonly TestDbContext DbContext; - - public PostsController(TestDbContext dbContext) - { - DbContext = dbContext; - } - - protected override IMaterializer MaterializerFactory() - { - return new EntityFrameworkMaterializer(DbContext); - } - } -} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs deleted file mode 100644 index b20ceb00..00000000 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/TagsController.cs +++ /dev/null @@ -1,21 +0,0 @@ -using JSONAPI.Core; -using JSONAPI.EntityFramework.Http; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; - -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers -{ - public class TagsController : ApiController - { - protected readonly TestDbContext DbContext; - - public TagsController(TestDbContext dbContext) - { - DbContext = dbContext; - } - - protected override IMaterializer MaterializerFactory() - { - return new EntityFrameworkMaterializer(DbContext); - } - } -} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs deleted file mode 100644 index b074f272..00000000 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Controllers/UsersController.cs +++ /dev/null @@ -1,21 +0,0 @@ -using JSONAPI.Core; -using JSONAPI.EntityFramework.Http; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; - -namespace JSONAPI.EntityFramework.Tests.TestWebApp.Controllers -{ - public class UsersController : ApiController - { - protected readonly TestDbContext DbContext; - - public UsersController(TestDbContext dbContext) - { - DbContext = dbContext; - } - - protected override IMaterializer MaterializerFactory() - { - return new EntityFrameworkMaterializer(DbContext); - } - } -} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs deleted file mode 100644 index 6dc55d2d..00000000 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Web; -using System.Web.Http; -using Autofac; -using Autofac.Integration.WebApi; -using JSONAPI.ActionFilters; -using JSONAPI.Core; -using JSONAPI.EntityFramework.ActionFilters; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using JSONAPI.Json; -using Microsoft.Owin; -using Owin; - -namespace JSONAPI.EntityFramework.Tests.TestWebApp -{ - public class Startup - { - private const string DbContextKey = "TestWebApp.DbContext"; - - private readonly Func _dbContextFactory; - - public Startup() - : this(context => new TestDbContext()) - { - - } - - public Startup(Func dbContextFactory) - { - _dbContextFactory = dbContextFactory; - } - - public void Configuration(IAppBuilder app) - { - // Setup db context for use in DI - app.Use(async (context, next) => - { - TestDbContext dbContext = _dbContextFactory(context); - context.Set(DbContextKey, dbContext); - - await next(); - - dbContext.Dispose(); - }); - - var appContainerBuilder = new ContainerBuilder(); - appContainerBuilder.Register(ctx => HttpContext.Current.GetOwinContext()).As(); - appContainerBuilder.Register(c => c.Resolve().Get(DbContextKey)).As(); - appContainerBuilder.RegisterApiControllers(Assembly.GetExecutingAssembly()); - var appContainer = appContainerBuilder.Build(); - app.UseAutofacMiddleware(appContainer); - - var httpConfig = GetWebApiConfiguration(); - httpConfig.DependencyResolver = new AutofacWebApiDependencyResolver(appContainer); - app.UseWebApi(httpConfig); - app.UseAutofacWebApi(httpConfig); - } - - private static HttpConfiguration GetWebApiConfiguration() - { - var config = new HttpConfiguration(); - - var pluralizationService = new PluralizationService(); - var modelManager = new ModelManager(pluralizationService); - - var formatter = new JsonApiFormatter(modelManager); - config.Formatters.Clear(); - config.Formatters.Add(formatter); - - // Global filters - config.Filters.Add(new EnumerateQueryableAsyncAttribute()); - config.Filters.Add(new EnableFilteringAttribute(modelManager)); - - // Web API routes - config.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional }); - - return config; - } - } -} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs deleted file mode 100644 index e2b8e502..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs +++ /dev/null @@ -1,155 +0,0 @@ -using System; -using System.Data.Common; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using FluentAssertions; -using JSONAPI.EntityFramework.Tests.TestWebApp; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using JSONAPI.Json; -using Microsoft.Owin.Testing; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace JSONAPI.EntityFramework.Tests.Acceptance -{ - [TestClass] - public abstract class AcceptanceTestsBase - { - private static readonly Uri BaseUri = new Uri("http://localhost"); - - protected static DbConnection GetEffortConnection() - { - return TestHelpers.GetEffortConnection(@"Acceptance\Data"); - } - - protected async Task TestGet(DbConnection effortConnection, string requestPath, string expectedResponseTextResourcePath) - { - using (var server = TestServer.Create(app => - { - var startup = new Startup(context => new TestDbContext(effortConnection, false)); - startup.Configuration(app); - })) - { - var uri = new Uri(BaseUri, requestPath); - var response = await server.CreateRequest(uri.ToString()).GetAsync(); - response.StatusCode.Should().Be(HttpStatusCode.OK); - var responseContent = await response.Content.ReadAsStringAsync(); - - var expected = - JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); - responseContent.Should().Be(expected); - } - } - - protected async Task TestGetWithFilter(DbConnection effortConnection, string requestPath, string expectedResponseTextResourcePath) - { - using (var server = TestServer.Create(app => - { - var startup = new Startup(context => new TestDbContext(effortConnection, false)); - startup.Configuration(app); - })) - { - var uri = new Uri(BaseUri, requestPath); - var response = await server.CreateRequest(uri.ToString()).GetAsync(); - response.StatusCode.Should().Be(HttpStatusCode.OK); - var responseContent = await response.Content.ReadAsStringAsync(); - - var expected = - JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); - responseContent.Should().Be(expected); - } - } - - protected async Task TestGetById(DbConnection effortConnection, string requestPath, string expectedResponseTextResourcePath) - { - using (var server = TestServer.Create(app => - { - var startup = new Startup(context => new TestDbContext(effortConnection, false)); - startup.Configuration(app); - })) - { - var uri = new Uri(BaseUri, requestPath); - var response = await server.CreateRequest(uri.ToString()).GetAsync(); - response.StatusCode.Should().Be(HttpStatusCode.OK); - var responseContent = await response.Content.ReadAsStringAsync(); - - var expected = - JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); - responseContent.Should().Be(expected); - } - } - - protected async Task TestPost(DbConnection effortConnection, string requestPath, string requestDataTextResourcePath, string expectedResponseTextResourcePath) - { - using (var server = TestServer.Create(app => - { - var startup = new Startup(context => new TestDbContext(effortConnection, false)); - startup.Configuration(app); - })) - { - var uri = new Uri(BaseUri, requestPath); - var requestContent = - JsonHelpers.MinifyJson( - TestHelpers.ReadEmbeddedFile(requestDataTextResourcePath)); - var response = await server - .CreateRequest(uri.ToString()) - .And(request => - { - request.Content = new StringContent(requestContent, Encoding.UTF8, "application/vnd.api+json"); - }) - .PostAsync(); - response.StatusCode.Should().Be(HttpStatusCode.OK); - var responseContent = await response.Content.ReadAsStringAsync(); - - var expected = - JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); - responseContent.Should().Be(expected); - } - } - - protected async Task TestPut(DbConnection effortConnection, string requestPath, string requestDataTextResourcePath, string expectedResponseTextResourcePath) - { - using (var server = TestServer.Create(app => - { - var startup = new Startup(context => new TestDbContext(effortConnection, false)); - startup.Configuration(app); - })) - { - var uri = new Uri(BaseUri, requestPath); - var requestContent = - JsonHelpers.MinifyJson( - TestHelpers.ReadEmbeddedFile(requestDataTextResourcePath)); - var response = await server - .CreateRequest(uri.ToString()) - .And(request => - { - request.Content = new StringContent(requestContent, Encoding.UTF8, - "application/vnd.api+json"); - }).SendAsync("PUT"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - var responseContent = await response.Content.ReadAsStringAsync(); - - var expected = - JsonHelpers.MinifyJson(TestHelpers.ReadEmbeddedFile(expectedResponseTextResourcePath)); - responseContent.Should().Be(expected); - } - } - - protected async Task TestDelete(DbConnection effortConnection, string requestPath) - { - using (var server = TestServer.Create(app => - { - var startup = new Startup(context => new TestDbContext(effortConnection, false)); - startup.Configuration(app); - })) - { - var uri = new Uri(BaseUri, requestPath); - var response = await server - .CreateRequest(uri.ToString()) - .SendAsync("DELETE"); - response.StatusCode.Should().Be(HttpStatusCode.NoContent); - } - } - } -} diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Data/User.csv b/JSONAPI.EntityFramework.Tests/Acceptance/Data/User.csv deleted file mode 100644 index f21d3f36..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Data/User.csv +++ /dev/null @@ -1,4 +0,0 @@ -Id,Name -"401","Alice" -"402","Bob" -"403","Charlie" \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_GetByIdResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_GetByIdResponse.json deleted file mode 100644 index 0141da6b..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_GetByIdResponse.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "posts": [ - { - "id": "202", - "title": "Post 2", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00", - "links": { - "author": "401", - "comments": [ "104" ], - "tags": [ "302", "303" ] - } - } - ] -} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_GetResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_GetResponse.json deleted file mode 100644 index c4f471e0..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_GetResponse.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "posts": [ - { - "id": "201", - "title": "Post 1", - "content": "Post 1 content", - "created": "2015-01-31T14:00:00+00:00", - "links": { - "author": "401", - "comments": [ "101", "102", "103" ], - "tags": [ "301", "302" ] - } - }, - { - "id": "202", - "title": "Post 2", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00", - "links": { - "author": "401", - "comments": [ "104" ], - "tags": [ "302", "303" ] - } - }, - { - "id": "203", - "title": "Post 3", - "content": "Post 3 content", - "created": "2015-02-07T11:11:00+00:00", - "links": { - "author": "401", - "comments": [ "105" ], - "tags": [ "303" ] - } - }, - { - "id": "204", - "title": "Post 4", - "content": "Post 4 content", - "created": "2015-02-08T06:59:00+00:00", - "links": { - "author": "402", - "comments": [], - "tags": [] - } - } - ] -} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_GetWithFilterResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_GetWithFilterResponse.json deleted file mode 100644 index a83c61f6..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_GetWithFilterResponse.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "posts": [ - { - "id": "204", - "title": "Post 4", - "content": "Post 4 content", - "created": "2015-02-08T06:59:00+00:00", - "links": { - "author": "402", - "comments": [], - "tags": [] - } - } - ] -} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PostRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PostRequest.json deleted file mode 100644 index a97e1a23..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PostRequest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "posts": [ - { - "id": "205", - "title": "Added post", - "content": "Added post content", - "created": "2015-03-11T04:31:00+00:00", - "links": { - "author": "401" - } - } - ] -} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PostResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PostResponse.json deleted file mode 100644 index 7322f663..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PostResponse.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "posts": [ - { - "id": "205", - "title": "Added post", - "content": "Added post content", - "created": "2015-03-11T04:31:00+00:00", - "links": { - "author": "401", - "comments": [], - "tags": [] - } - } - ] -} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PutRequest.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PutRequest.json deleted file mode 100644 index 41f00896..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PutRequest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "posts": [ - { - "id": "202", - "title": "New post title" - } - ] -} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PutResponse.json b/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PutResponse.json deleted file mode 100644 index 03e7d034..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts_PutResponse.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "posts": [ - { - "id": "202", - "title": "New post title", - "content": "Post 2 content", - "created": "2015-02-05T08:10:00+00:00", - "links": { - "author": "401", - "comments": [ "104" ], - "tags": [ "302", "303" ] - } - } - ] -} \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs b/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs deleted file mode 100644 index e381f898..00000000 --- a/JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using FluentAssertions; -using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Post = JSONAPI.EntityFramework.Tests.TestWebApp.Models.Post; - -namespace JSONAPI.EntityFramework.Tests.Acceptance -{ - [TestClass] - public class PostsTests : AcceptanceTestsBase - { - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Get() - { - using (var effortConnection = GetEffortConnection()) - { - await TestGet(effortConnection, "posts", @"Acceptance\Fixtures\Posts_GetResponse.json"); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task GetWithFilter() - { - using (var effortConnection = GetEffortConnection()) - { - await TestGetWithFilter(effortConnection, "posts?title=Post 4", @"Acceptance\Fixtures\Posts_GetWithFilterResponse.json"); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task GetById() - { - using (var effortConnection = GetEffortConnection()) - { - await TestGetById(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts_GetByIdResponse.json"); - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Post() - { - using (var effortConnection = GetEffortConnection()) - { - await TestPost(effortConnection, "posts", @"Acceptance\Fixtures\Posts_PostRequest.json", @"Acceptance\Fixtures\Posts_PostResponse.json"); - - using (var dbContext = new TestDbContext(effortConnection, false)) - { - var allPosts = dbContext.Posts.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))); - actualPost.AuthorId.Should().Be("401"); - } - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Put() - { - using (var effortConnection = GetEffortConnection()) - { - await TestPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts_PutRequest.json", @"Acceptance\Fixtures\Posts_PutResponse.json"); - - using (var dbContext = new TestDbContext(effortConnection, false)) - { - var allPosts = dbContext.Posts.ToArray(); - allPosts.Length.Should().Be(4); - var actualPost = allPosts.First(t => t.Id == "202"); - actualPost.Id.Should().Be("202"); - actualPost.Title.Should().Be("New post title"); - actualPost.Content.Should().Be("Post 2 content"); - actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0))); - actualPost.AuthorId.Should().Be("401"); - } - } - } - - [TestMethod] - [DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")] - [DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")] - public async Task Delete() - { - using (var effortConnection = GetEffortConnection()) - { - await TestDelete(effortConnection, "posts/203"); - - using (var dbContext = new TestDbContext(effortConnection, false)) - { - var allTodos = dbContext.Posts.ToArray(); - allTodos.Length.Should().Be(3); - var actualTodo = allTodos.FirstOrDefault(t => t.Id == "203"); - actualTodo.Should().BeNull(); - } - } - } - } -} diff --git a/JSONAPI.EntityFramework.Tests/ActionFilters/EnumerateQueryableAsyncAttributeTests.cs b/JSONAPI.EntityFramework.Tests/ActionFilters/AsynchronousEnumerationTransformerTests.cs similarity index 61% rename from JSONAPI.EntityFramework.Tests/ActionFilters/EnumerateQueryableAsyncAttributeTests.cs rename to JSONAPI.EntityFramework.Tests/ActionFilters/AsynchronousEnumerationTransformerTests.cs index 42f86fd1..609c95ed 100644 --- a/JSONAPI.EntityFramework.Tests/ActionFilters/EnumerateQueryableAsyncAttributeTests.cs +++ b/JSONAPI.EntityFramework.Tests/ActionFilters/AsynchronousEnumerationTransformerTests.cs @@ -6,7 +6,6 @@ using System.Net; using System.Net.Http; using System.Net.Http.Formatting; -using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web.Http.Controllers; @@ -20,7 +19,7 @@ namespace JSONAPI.EntityFramework.Tests.ActionFilters { [TestClass] - public class EnumerateQueryableAsyncAttributeTests + public class AsynchronousEnumerationTransformerTests { public class Dummy { @@ -54,7 +53,7 @@ public void SetupFixtures() }.AsQueryable(); } - private HttpActionExecutedContext CreateActionExecutedContext(IDbAsyncEnumerator asyncEnumerator) + private IQueryable CreateQueryable(IDbAsyncEnumerator asyncEnumerator) { var mockSet = new Mock>(); mockSet.As>() @@ -69,39 +68,17 @@ private HttpActionExecutedContext CreateActionExecutedContext(IDbAsyncEnumerator mockSet.As>().Setup(m => m.ElementType).Returns(_fixtures.ElementType); mockSet.As>().Setup(m => m.GetEnumerator()).Returns(_fixtures.GetEnumerator()); - var formatter = new JsonMediaTypeFormatter(); - - var httpContent = new ObjectContent(typeof(IQueryable), mockSet.Object, formatter); - - return new HttpActionExecutedContext - { - ActionContext = new HttpActionContext - { - ControllerContext = new HttpControllerContext - { - Request = new HttpRequestMessage(HttpMethod.Get, "http://api.example.com/dummies") - } - }, - Response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = httpContent - } - }; + return mockSet.Object; } [TestMethod] public async Task ResolvesQueryable() { - var actionFilter = new EnumerateQueryableAsyncAttribute(); - - var context = CreateActionExecutedContext(new TestDbAsyncEnumerator(_fixtures.GetEnumerator())); - - await actionFilter.OnActionExecutedAsync(context, new CancellationToken()); + var transformer = new AsynchronousEnumerationTransformer(); - var objectContent = context.Response.Content as ObjectContent; - objectContent.Should().NotBeNull(); + var query = CreateQueryable(new TestDbAsyncEnumerator(_fixtures.GetEnumerator())); - var array = objectContent.Value as Dummy[]; + var array = await transformer.Enumerate(query, new CancellationToken()); array.Should().NotBeNull(); array.Length.Should().Be(3); array[0].Id.Should().Be("1"); @@ -112,16 +89,16 @@ public async Task ResolvesQueryable() [TestMethod] public void CancelsProperly() { - var actionFilter = new EnumerateQueryableAsyncAttribute(); + var actionFilter = new AsynchronousEnumerationTransformer(); - var context = CreateActionExecutedContext(new WaitsUntilCancellationDbAsyncEnumerator(1000, _fixtures.GetEnumerator())); + var context = CreateQueryable(new WaitsUntilCancellationDbAsyncEnumerator(1000, _fixtures.GetEnumerator())); var cts = new CancellationTokenSource(); cts.CancelAfter(300); Func action = async () => { - await actionFilter.OnActionExecutedAsync(context, cts.Token); + await actionFilter.Enumerate(context, cts.Token); }; action.ShouldThrow(); } diff --git a/JSONAPI.EntityFramework.Tests/App.Config b/JSONAPI.EntityFramework.Tests/App.Config index 7548116e..75b64305 100644 --- a/JSONAPI.EntityFramework.Tests/App.Config +++ b/JSONAPI.EntityFramework.Tests/App.Config @@ -1,12 +1,9 @@  -
- - - - + + diff --git a/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs b/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs new file mode 100644 index 00000000..8548b6bd --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/DbContextExtensionsTests.cs @@ -0,0 +1,131 @@ +using System; +using System.Data.Common; +using System.Linq; +using Effort; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using JSONAPI.EntityFramework.Tests.Models; +using FluentAssertions; +using System.Collections.Generic; +using System.Data.Entity; + +namespace JSONAPI.EntityFramework.Tests +{ + [TestClass] + public class DbContextExtensionsTests + { + private class TestDbContext : DbContext + { + public DbSet Backlinks { get; set; } + public DbSet Posts { get; set; } + public DbSet PostIDs { get; set; } + + public TestDbContext(DbConnection conn) : base(conn, true) + { + + } + } + + private class NotAnEntity + { + public string Id { get; set; } + public string Temporary { get; set; } + } + + private class SubPost : Post + { + public string Foo { get; set; } + } + private class SubPostID : PostID + { + public string Foo { get; set; } + } + + private DbConnection _conn; + private TestDbContext _context; + + [TestInitialize] + public void SetupEntities() + { + _conn = DbConnectionFactory.CreateTransient(); + _context = new TestDbContext(_conn); + + var b1 = new Backlink + { + Url = "http://www.google.com/", + Snippet = "1 Results" + }; + _context.Backlinks.Add(b1); + + _context.SaveChanges(); + } + + [TestCleanup] + private void CleanupTest() + { + _context.Dispose(); + } + + [TestMethod] + public void GetKeyNamesStandardIdTest() + { + // Act + IEnumerable keyNames = _context.GetKeyNames(typeof(Post)).ToArray(); + + // Assert + keyNames.Count().Should().Be(1); + keyNames.First().Should().Be("Id"); + } + + [TestMethod] + public void GetKeyNamesStandardIDTest() + { + // Act + IEnumerable keyNames = _context.GetKeyNames(typeof(PostID)).ToArray(); + + // Assert + keyNames.Count().Should().Be(1); + keyNames.First().Should().Be("ID"); + } + + [TestMethod] + public void GetKeyNamesNonStandardIdTest() + { + // Act + IEnumerable keyNames = _context.GetKeyNames(typeof(Backlink)).ToArray(); + + // Assert + keyNames.Count().Should().Be(1); + keyNames.First().Should().Be("Url"); + } + + [TestMethod] + public void GetKeyNamesForChildClass() + { + // Act + IEnumerable keyNames = _context.GetKeyNames(typeof(SubPost)).ToArray(); + + // Assert + keyNames.Count().Should().Be(1); + keyNames.First().Should().Be("Id"); + } + + [TestMethod] + public void GetKeyNamesForChildClassID() + { + // Act + IEnumerable keyNames = _context.GetKeyNames(typeof(SubPostID)).ToArray(); + + // Assert + keyNames.Count().Should().Be(1); + keyNames.First().Should().Be("ID"); + } + + [TestMethod] + public void GetKeyNamesNotAnEntityTest() + { + // Act + Action action = () => _context.GetKeyNames(typeof (NotAnEntity)); + action.ShouldThrow().Which.Message.Should().Be("Failed to identify the key names for NotAnEntity or any of its parent classes."); + } + } +} diff --git a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs b/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs deleted file mode 100644 index 0e5f1a22..00000000 --- a/JSONAPI.EntityFramework.Tests/EntityConverterTests.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.Text; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; -using JSONAPI.Core; -using JSONAPI.Json; -using JSONAPI.EntityFramework; -using JSONAPI.EntityFramework.Tests.Models; -using System.IO; -using System.Diagnostics; -using System.Reflection; -using System.Data; -using System.Text.RegularExpressions; - -namespace JSONAPI.EntityFramework.Tests -{ - [TestClass] - public class EntityConverterTests - { - private TestEntities context; - private Author a, a2; - private Post p, p2, p3; - private Comment c, c2, c3, c4; - - [TestInitialize] - public void SetupEntities() - { - //- See http://stackoverflow.com/a/19130718/489116 - var instance = System.Data.Entity.SqlServer.SqlProviderServices.Instance; - //- - - context = new TestEntities(); - //JSONAPI.EntityFramework.Json.ContractResolver.ObjectContext = context; - - - // Clear it out! - foreach (Comment o in context.Comments) context.Comments.Remove(o); - foreach (Post o in context.Posts) context.Posts.Remove(o); - foreach (Author o in context.Authors) context.Authors.Remove(o); - context.SaveChanges(); - - - a = new Author - { - Id = Guid.NewGuid().ToString(), - Name = "Jason Hater" - }; - context.Authors.Add(a); - - p = new Post() - { - Title = "Linkbait!", - Author = a - }; - p2 = new Post - { - Title = "Rant #1023", - Author = a - }; - p3 = new Post - { - Title = "Polemic in E-flat minor #824", - Author = a - }; - - a.Posts.Add(p); - a.Posts.Add(p2); - a.Posts.Add(p3); - - p.Comments.Add( - c = new Comment() - { - Body = "Nuh uh!", - Post = p - } - ); - p.Comments.Add( - c2 = new Comment() - { - Body = "Yeah huh!", - Post = p - } - ); - p.Comments.Add( - c3 = new Comment() - { - Body = "Third Reich.", - Post = p - } - ); - - p2.Comments.Add( - c4 = new Comment - { - Body = "I laughed, I cried!", - Post = p2 - } - ); - - context.SaveChanges(); - } - - [TestMethod] - public void SerializeTest() - { - // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); - MemoryStream stream = new MemoryStream(); - - // Act - formatter.WriteToStreamAsync(typeof(Post), p.Comments.First(), stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); - - // Assert - string output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - Trace.WriteLine(output); - - } - - [TestMethod] - [DeploymentItem(@"Data\Post.json")] - public async Task DeserializePostIntegrationTest() - { - // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); - MemoryStream stream = new MemoryStream(); - - EntityFrameworkMaterializer materializer = new EntityFrameworkMaterializer(context); - - // Serialize a post and change the JSON... Not "unit" at all, I know, but a good integration test I think... - formatter.WriteToStreamAsync(typeof(Post), p, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); - string serializedPost = System.Text.Encoding.ASCII.GetString(stream.ToArray()); - // Change the post title (a scalar value) - serializedPost = serializedPost.Replace("Linkbait!", "Not at all linkbait!"); - // Remove a comment (Note that order is undefined/not deterministic!) - serializedPost = Regex.Replace(serializedPost, String.Format(@"(""comments""\s*:\s*\[[^]]*)(,""{0}""|""{0}"",)", c3.Id), @"$1"); - - // Reread the serialized JSON... - stream.Dispose(); - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(serializedPost)); - - // Act - Post pUpdated; - pUpdated = (Post)await formatter.ReadFromStreamAsync(typeof(Post), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null); - pUpdated = await materializer.MaterializeUpdateAsync(pUpdated); - - // Assert - Assert.AreEqual(a, pUpdated.Author); - Assert.AreEqual("Not at all linkbait!", pUpdated.Title); - Assert.AreEqual(2, pUpdated.Comments.Count()); - Assert.IsFalse(pUpdated.Comments.Contains(c3)); - //Debug.WriteLine(sw.ToString()); - } - - [TestMethod] - public async Task UnderpostingTest() - { - // Arrange - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); - MemoryStream stream = new MemoryStream(); - - EntityFrameworkMaterializer materializer = new EntityFrameworkMaterializer(context); - - string underpost = @"{""posts"":{""id"":""" + p.Id.ToString() + @""",""title"":""Not at all linkbait!""}}"; - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(underpost)); - - int previousCommentsCount = p.Comments.Count; - - // Act - Post pUpdated; - pUpdated = (Post)await formatter.ReadFromStreamAsync(typeof(Post), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null); - pUpdated = await materializer.MaterializeUpdateAsync(pUpdated); - - // Assert - Assert.AreEqual(previousCommentsCount, pUpdated.Comments.Count, "Comments were wiped out!"); - Assert.AreEqual("Not at all linkbait!", pUpdated.Title, "Title was not updated."); - } - - } -} diff --git a/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs b/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs deleted file mode 100644 index 52a88af4..00000000 --- a/JSONAPI.EntityFramework.Tests/EntityFrameworkMaterializerTests.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Linq; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using JSONAPI.EntityFramework.Tests.Models; -using FluentAssertions; -using System.Collections.Generic; -using System.Data.Entity; - -namespace JSONAPI.EntityFramework.Tests -{ - [TestClass] - public class EntityFrameworkMaterializerTests - { - private class TestDbContext : DbContext - { - public DbSet Backlinks { get; set; } - public DbSet Posts { get; set; } - } - - private class NotAnEntity - { - public string Id { get; set; } - public string Temporary { get; set; } - } - - private TestDbContext context; - private Backlink b1, b2; - - [TestInitialize] - public void SetupEntities() - { - //- See http://stackoverflow.com/a/19130718/489116 - var instance = System.Data.Entity.SqlServer.SqlProviderServices.Instance; - //- - - context = new TestDbContext(); - //JSONAPI.EntityFramework.Json.ContractResolver.ObjectContext = context; - - - // Clear it out! - foreach (Backlink o in context.Backlinks) context.Backlinks.Remove(o); - context.SaveChanges(); - - b1 = new Backlink - { - Url = "http://www.google.com/", - Snippet = "1 Results" - }; - - context.SaveChanges(); - } - - [TestMethod] - public void GetKeyNamesStandardIdTest() - { - // Arrange - var materializer = new EntityFrameworkMaterializer(context); - - // Act - IEnumerable keyNames = materializer.GetKeyNames(typeof(Post)); - - // Assert - keyNames.Count().Should().Be(1); - keyNames.First().Should().Be("Id"); - } - - [TestMethod] - public void GetKeyNamesNonStandardIdTest() - { - // Arrange - var materializer = new EntityFrameworkMaterializer(context); - - // Act - IEnumerable keyNames = materializer.GetKeyNames(typeof(Backlink)); - - // Assert - keyNames.Count().Should().Be(1); - keyNames.First().Should().Be("Url"); - } - - [TestMethod] - public void GetKeyNamesNotAnEntityTest() - { - // Arrange - var materializer = new EntityFrameworkMaterializer(context); - - // Act - Action action = () => - { - materializer.GetKeyNames(typeof (NotAnEntity)); - }; - action.ShouldThrow().Which.Message.Should().Be("The Type NotAnEntity was not found in the DbContext with Type TestDbContext"); - } - } -} diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index f2593c70..6e341345 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -22,7 +22,7 @@ true full false - C:\temp\JSONAPI.EntityFramework.Tests\bin\Debug\ + bin\Debug\ DEBUG;TRACE prompt 4 @@ -38,16 +38,17 @@ false - + + False ..\packages\Effort.EF6.1.1.4\lib\net45\Effort.dll False - ..\packages\EntityFramework.6.1.2\lib\net45\EntityFramework.dll + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll False - ..\packages\EntityFramework.6.1.2\lib\net45\EntityFramework.SqlServer.dll + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll False @@ -75,7 +76,8 @@ False ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll - + + False ..\packages\NMemory.1.0.1\lib\net45\NMemory.dll @@ -108,11 +110,8 @@ - - - - - + + @@ -120,37 +119,14 @@ - + - Designer - - - - - - - - - Always - - - Always - - - Always - - - Always - - - Always - Always @@ -160,9 +136,9 @@ - + {76dee472-723b-4be6-8b97-428ac326e30f} - JSONAPI.EntityFramework.Tests.TestWebApp + JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp {E906356C-93F6-41F6-9A0D-73B8A99AA53C} diff --git a/JSONAPI.EntityFramework.Tests/Models/PostID.cs b/JSONAPI.EntityFramework.Tests/Models/PostID.cs new file mode 100644 index 00000000..420e047a --- /dev/null +++ b/JSONAPI.EntityFramework.Tests/Models/PostID.cs @@ -0,0 +1,21 @@ +namespace JSONAPI.EntityFramework.Tests.Models +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations.Schema; + + public partial class PostID + { + public PostID() + { + this.Comments = new HashSet(); + } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid ID { get; set; } + public string Title { get; set; } + + public virtual Author Author { get; set; } + public virtual ICollection Comments { get; set; } + } +} diff --git a/JSONAPI.EntityFramework.Tests/Models/TestEntities.cs b/JSONAPI.EntityFramework.Tests/Models/TestEntities.cs deleted file mode 100644 index 67b61d3b..00000000 --- a/JSONAPI.EntityFramework.Tests/Models/TestEntities.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace JSONAPI.EntityFramework.Tests.Models -{ - using System; - using System.Data.Entity; - using System.Data.Entity.Infrastructure; - - public partial class TestEntities : DbContext - { - private class TestEntitiesInitializer : DropCreateDatabaseIfModelChanges { } - - public TestEntities() - : base("name=TestEntities") - { - } - - protected override void OnModelCreating(DbModelBuilder modelBuilder) - { - Database.SetInitializer(new TestEntitiesInitializer()); - } - - public virtual DbSet Authors { get; set; } - public virtual DbSet Posts { get; set; } - public virtual DbSet Comments { get; set; } - public virtual DbSet Backlinks { get; set; } - } -} diff --git a/JSONAPI.EntityFramework.Tests/packages.config b/JSONAPI.EntityFramework.Tests/packages.config index df24cfb7..a5ce68c2 100644 --- a/JSONAPI.EntityFramework.Tests/packages.config +++ b/JSONAPI.EntityFramework.Tests/packages.config @@ -1,7 +1,7 @@  - + diff --git a/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs b/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs new file mode 100644 index 00000000..f6efe4c3 --- /dev/null +++ b/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs @@ -0,0 +1,24 @@ +using System.Data.Entity; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.QueryableTransformers; + +namespace JSONAPI.EntityFramework.ActionFilters +{ + /// + /// Enumerates an IQueryable asynchronously using Entity Framework's ToArrayAsync() method. + /// + public class AsynchronousEnumerationTransformer : IQueryableEnumerationTransformer + { + public async Task Enumerate(IQueryable query, CancellationToken cancellationToken) + { + return await query.ToArrayAsync(cancellationToken); + } + + public async Task FirstOrDefault(IQueryable query, CancellationToken cancellationToken) + { + return await query.FirstOrDefaultAsync(cancellationToken); + } + } +} diff --git a/JSONAPI.EntityFramework/ActionFilters/EnumerateQueryableAsyncAttribute.cs b/JSONAPI.EntityFramework/ActionFilters/EnumerateQueryableAsyncAttribute.cs deleted file mode 100644 index 40e0f47b..00000000 --- a/JSONAPI.EntityFramework/ActionFilters/EnumerateQueryableAsyncAttribute.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Data.Entity; -using System.Linq; -using System.Net.Http; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using System.Web.Http.Filters; - -namespace JSONAPI.EntityFramework.ActionFilters -{ - public class EnumerateQueryableAsyncAttribute : ActionFilterAttribute - { - private readonly Lazy _toArrayAsyncMethod = new Lazy(() => - typeof(QueryableExtensions).GetMethods().FirstOrDefault(x => x.Name == "ToArrayAsync" && x.GetParameters().Count() == 2)); - - public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) - { - if (actionExecutedContext.Response != null) - { - var objectContent = actionExecutedContext.Response.Content as ObjectContent; - if (objectContent != null) - { - var objectType = objectContent.ObjectType; - if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IQueryable<>)) - { - var queryableElementType = objectType.GenericTypeArguments[0]; - var openToArrayAsyncMethod = _toArrayAsyncMethod.Value; - var toArrayAsyncMethod = openToArrayAsyncMethod.MakeGenericMethod(queryableElementType); - var invocation = (dynamic)toArrayAsyncMethod.Invoke(null, new[] { objectContent.Value, cancellationToken }); - - var resultArray = await invocation; - actionExecutedContext.Response.Content = new ObjectContent(resultArray.GetType(), resultArray, objectContent.Formatter); - } - } - } - } - } -} diff --git a/JSONAPI.EntityFramework/App.config b/JSONAPI.EntityFramework/App.config index 70ad9000..5dcb1e0c 100644 --- a/JSONAPI.EntityFramework/App.config +++ b/JSONAPI.EntityFramework/App.config @@ -1,9 +1,9 @@  -
- + + diff --git a/JSONAPI.EntityFramework/Configuration/JsonApiAutofacConfigurationExtensions.cs b/JSONAPI.EntityFramework/Configuration/JsonApiAutofacConfigurationExtensions.cs new file mode 100644 index 00000000..eca72b87 --- /dev/null +++ b/JSONAPI.EntityFramework/Configuration/JsonApiAutofacConfigurationExtensions.cs @@ -0,0 +1,20 @@ +using System; +using JSONAPI.Configuration; +using JSONAPI.EntityFramework.Http; + +namespace JSONAPI.EntityFramework.Configuration +{ + public static class JsonApiConfigurationExtensions + { + public static void RegisterEntityFrameworkResourceType(this JsonApiConfiguration jsonApiConfiguration, + Action> configurationAction = null) where TResourceType : class + { + jsonApiConfiguration.RegisterResourceType(c => + { + c.UseDocumentMaterializer>(); + if (configurationAction != null) + configurationAction(c); + }); + } + } +} diff --git a/JSONAPI.EntityFramework/DbContextExtensions.cs b/JSONAPI.EntityFramework/DbContextExtensions.cs new file mode 100644 index 00000000..37724b77 --- /dev/null +++ b/JSONAPI.EntityFramework/DbContextExtensions.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Data.Entity.Core.Objects; +using System.Data.Entity.Infrastructure; +using System.Linq; +using System.Reflection; + +namespace JSONAPI.EntityFramework +{ + /// + /// Extensions on DbContext useful for JSONAPI.NET + /// + public static class DbContextExtensions + { + /// + /// Gets the ID key names for an entity type + /// + /// + /// + /// + public static IEnumerable GetKeyNames(this DbContext dbContext, Type type) + { + if (dbContext == null) throw new ArgumentNullException("dbContext"); + if (type == null) throw new ArgumentNullException("type"); + + var originalType = type; + + while (type != null) + { + var openMethod = typeof(DbContextExtensions).GetMethod("GetKeyNamesFromGeneric", BindingFlags.Public | BindingFlags.Static); + var method = openMethod.MakeGenericMethod(type); + + try + { + return (IEnumerable) method.Invoke(null, new object[] {dbContext}); + } + catch (TargetInvocationException) + { + } + + type = type.BaseType; + } + + throw new Exception(string.Format("Failed to identify the key names for {0} or any of its parent classes.", originalType.Name)); + } + + /// + /// Gets the ID key names for an entity type + /// + /// + /// + /// + /// + public static IEnumerable GetKeyNamesFromGeneric(this DbContext dbContext) where T : class + { + var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext; + ObjectSet objectSet; + try + { + objectSet = objectContext.CreateObjectSet(); + + } + catch (InvalidOperationException e) + { + throw new ArgumentException( + String.Format("The Type {0} was not found in the DbContext with Type {1}", typeof(T).Name, dbContext.GetType().Name), + e + ); + } + return objectSet.EntitySet.ElementType.KeyMembers.Select(k => k.Name).ToArray(); + } + + } +} diff --git a/JSONAPI.EntityFramework/Documents/Builders/EntityFrameworkQueryableResourceCollectionDocumentBuilder.cs b/JSONAPI.EntityFramework/Documents/Builders/EntityFrameworkQueryableResourceCollectionDocumentBuilder.cs new file mode 100644 index 00000000..1bd9e2c8 --- /dev/null +++ b/JSONAPI.EntityFramework/Documents/Builders/EntityFrameworkQueryableResourceCollectionDocumentBuilder.cs @@ -0,0 +1,55 @@ +using System; +using System.Data.Entity; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; +using JSONAPI.Http; +using JSONAPI.QueryableTransformers; + +namespace JSONAPI.EntityFramework.Documents.Builders +{ + /// + /// Provides a entity framework implementation of an IQueryableResourceCollectionDocumentBuilder + /// + public class EntityFrameworkQueryableResourceCollectionDocumentBuilder: DefaultQueryableResourceCollectionDocumentBuilder + { + /// + /// Creates a new EntityFrameworkQueryableResourceCollectionDocumentBuilder + /// + public EntityFrameworkQueryableResourceCollectionDocumentBuilder( + IResourceCollectionDocumentBuilder resourceCollectionDocumentBuilder, + IQueryableEnumerationTransformer enumerationTransformer, + IQueryableFilteringTransformer filteringTransformer, + IQueryableSortingTransformer sortingTransformer, + IQueryablePaginationTransformer paginationTransformer, + IBaseUrlService baseUrlService) : + base(resourceCollectionDocumentBuilder, + enumerationTransformer, + filteringTransformer, + sortingTransformer, + paginationTransformer, + baseUrlService) + { + } + + /// + /// Returns the metadata that should be sent with this document. + /// + protected override async Task GetDocumentMetadata(IQueryable originalQuery, IQueryable filteredQuery, IOrderedQueryable sortedQuery, + IPaginationTransformResult paginationResult, CancellationToken cancellationToken) + { + var metadata = new Metadata(); + if (paginationResult.PaginationWasApplied) + { + var count = await filteredQuery.CountAsync(cancellationToken); + metadata.MetaObject.Add("total-pages", (int)Math.Ceiling((decimal) count / paginationResult.PageSize)); + metadata.MetaObject.Add("total-count", count); + } + if (metadata.MetaObject.HasValues) + return metadata; + return null; + } + } +} diff --git a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs deleted file mode 100644 index c9810fb8..00000000 --- a/JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs +++ /dev/null @@ -1,372 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Data.Entity; -using JSONAPI.Core; -using System.Data.Entity.Core.Objects; -using System.Data.Entity.Infrastructure; -using System.Reflection; -using System.Collections; -using System.Data.Entity.Core; -using JSONAPI.Extensions; - -namespace JSONAPI.EntityFramework -{ - public partial class EntityFrameworkMaterializer : IMaterializer - { - private DbContext context; - - public DbContext DbContext - { - get { return this.context; } - } - - public EntityFrameworkMaterializer(DbContext context) : base() - { - this.context = context; - } - - #region IMaterializer contract methods - - /// - /// Override here if you have mixed models and need to have child objects that are not supposed to be entities! - /// - /// - /// - /// - public virtual Task GetByIdAsync(Type type, params Object[] idValues) - { - //TODO: How to react if the type isn't in the context? - - // Input will probably usually be strings... make sure the right types are passed to .Find()... - Object[] idv2 = new Object[idValues.Length]; - int i = 0; - foreach (PropertyInfo prop in GetKeyProperties(type)) - { - try - { - idv2[i] = ((IConvertible)idValues[i]).ToType(prop.PropertyType, null); - } - catch (Exception e) - { - // Fallback...use what they gave us... - idv2[i] = idValues[i]; - } - finally - { - i++; - } - } - return this.context.Set(type).FindAsync(idv2); - } - - public async Task GetByIdAsync(params Object[] idValues) - { - return (T) await GetByIdAsync(typeof(T), idValues); - } - - public async Task MaterializeAsync(T ephemeral) - { - return (T) await MaterializeAsync(typeof(T), ephemeral); - } - - public virtual async Task MaterializeAsync(Type type, object ephemeral) - { - IEnumerable keyNames = GetKeyNames(type); - List idValues = new List(); - bool anyNull = false; - foreach (string propName in keyNames) - { - PropertyInfo propInfo = type.GetProperty(propName); - Object value = propInfo.GetValue(ephemeral); - if (value == null) - { - anyNull = true; - break; - } - idValues.Add(value); - } - object retval = null; - if (!anyNull) - { - retval = await context.Set(type).FindAsync(idValues.ToArray()); - } - if (retval == null) - { - // Didn't find it...create a new one! - retval = Activator.CreateInstance(type); - context.Set(type).Add(retval); - if (!anyNull) - { - // For a new object, if a key is specified, we want to merge the key, at least. - // For simplicity then, make the behavior equivalent to MergeMaterialize in this case. - await this.Merge(type, ephemeral, retval); - } - } - return retval; - } - - public async Task MaterializeUpdateAsync(T ephemeral) - { - return (T) await MaterializeUpdateAsync(typeof(T), ephemeral); - } - - public async Task MaterializeUpdateAsync(Type type, object ephemeral) - { - object material = await MaterializeAsync(type, ephemeral); - await this.Merge(type, ephemeral, material); - return material; - } - - #endregion - - #region Obsolete IMaterializer contract methods - - public T GetById(params object[] keyValues) - { - return GetByIdAsync(keyValues).Result; - } - - public object GetById(Type type, params object[] keyValues) - { - return GetByIdAsync(type, keyValues).Result; - } - - public T Materialize(T ephemeral) - { - return MaterializeAsync(ephemeral).Result; - } - - public object Materialize(Type type, object ephemeral) - { - return MaterializeAsync(type, ephemeral).Result; - } - - public T MaterializeUpdate(T ephemeral) - { - return MaterializeUpdateAsync(ephemeral).Result; - } - - public object MaterializeUpdate(Type type, object ephemeral) - { - return MaterializeUpdateAsync(type, ephemeral).Result; - } - - #endregion - - private bool IsMany(Type type) - { - //TODO: Should we check for arrays also? (They aren't generics.) - return (type.GetInterfaces().Contains(typeof(IEnumerable)) && type.IsGenericType); - } - - public bool IsModel(Type objectType) - { - if (objectType.CanWriteAsJsonApiAttribute()) - return false; - else return true; - } - - private Type GetSingleType(Type type) - { - if (IsMany(type)) - return type.IsGenericType ? type.GenericTypeArguments[0] : type.GetElementType(); - else - return type; - } - - protected internal virtual IEnumerable GetKeyNames(Type type) - { - var openMethod = typeof (EntityFrameworkMaterializer).GetMethod("GetKeyNamesFromGeneric", BindingFlags.NonPublic | BindingFlags.Static); - var method = openMethod.MakeGenericMethod(type); - try - { - return (IEnumerable)method.Invoke(null, new object[] { this.context }); - } - catch (TargetInvocationException ex) - { - throw ex.InnerException; - } - } - - // ReSharper disable once UnusedMember.Local - private static IEnumerable GetKeyNamesFromGeneric(DbContext dbContext) where T : class - { - var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext; - ObjectSet objectSet; - try - { - objectSet = objectContext.CreateObjectSet(); - - } - catch (InvalidOperationException e) - { - throw new ArgumentException( - String.Format("The Type {0} was not found in the DbContext with Type {1}", typeof(T).Name, dbContext.GetType().Name), - e - ); - } - return objectSet.EntitySet.ElementType.KeyMembers.Select(k => k.Name).ToArray(); - } - - /// - /// Convenience wrapper around GetKeyNames. - /// - /// - /// - private IEnumerable GetKeyProperties(Type type) - { - IEnumerable keyNames = GetKeyNames(type); - List retval = new List(); - foreach (string propName in keyNames) - { - retval.Add(type.GetProperty(propName)); - } - return retval; - } - - protected string GetEntitySetName(Type type) - { - ObjectContext objectContext = ((IObjectContextAdapter)this.context).ObjectContext; - try - { - var container = objectContext.MetadataWorkspace - .GetEntityContainer(objectContext.DefaultContainerName, System.Data.Entity.Core.Metadata.Edm.DataSpace.CSpace); - // If this fails, type must not be in the context!!! - string esname = (from meta in container.BaseEntitySets - where meta.ElementType.Name == type.Name - select meta.Name).FirstOrDefault(); - if (esname == null) - { - return null; - } - return container.Name + "." + esname; - } - catch (Exception e) - { - // Type is not an entity type in the context. Return null. - return null; - } - } - - /// - /// Will return an entity key for any object. If it is not actually an entity (that is, the type - /// is not found in the context as some DbSet), it will create a dummy EntityKey based on whatever - /// GetKeyNames says is the primary key. Override GetKeyNames to make this work properly for your - /// own model objects, see GetKeyNames for more information. - /// - /// - /// - /// - protected EntityKey MaterializeEntityKey(Type type, object obj) - { - IEnumerable keyProps = GetKeyProperties(type); - - // Build the key pairs... - IList> entityKeyValues = - new List>(); - foreach (PropertyInfo propertyInfo in keyProps) - { - entityKeyValues.Add( - new KeyValuePair( - propertyInfo.Name, - propertyInfo.GetValue(obj, null) - ) - ); - } - - string esname = GetEntitySetName(type); - // esname does not have to be real! - if (esname == null) esname = type.Namespace + "." + type.Name; - - EntityKey key = new EntityKey(esname, entityKeyValues); - - return key; - } - - private async Task Merge (Type type, object ephemeral, object material) - { - PropertyInfo[] props = type.GetProperties(); - foreach (PropertyInfo prop in props) - { - // Comply with the spec, if a key was not set, it should not be updated! - if (!MetadataManager.Instance.PropertyWasPresent(ephemeral, prop)) continue; - - if (IsMany(prop.PropertyType)) - { - Type elementType = GetSingleType(prop.PropertyType); - IEnumerable keyNames = GetKeyNames(elementType); - - var materialMany = (IEnumerable)prop.GetValue(material, null); - var ephemeralMany = (IEnumerable)prop.GetValue(ephemeral, null); - - var materialKeys = new HashSet(); - var ephemeralKeys = new HashSet(); - foreach (object child in materialMany) - { - //TODO: Faster to get the Entity Key from the object context? This seems like it may be just as efficient... - // see http://social.msdn.microsoft.com/Forums/en-US/479f2b2e-fdaa-4d9d-91da-772ba0ee8d55/ef40-how-to-get-entitykey-from-new-poco-?forum=adonetefx - materialKeys.Add(MaterializeEntityKey(elementType, child)); - } - foreach (object child in ephemeralMany) - { - ephemeralKeys.Add(MaterializeEntityKey(elementType, child)); - } - - // We're having problems with how to generalize/cast/generic-ize this code, so for the time - // being we'll brute-force it in super-dynamic language style... - Type mmtype = materialMany.GetType(); - MethodInfo mmadd = mmtype.GetMethod("Add"); - MethodInfo mmremove = mmtype.GetMethod("Remove"); - - // Add to hasMany - if (mmadd != null) - foreach (EntityKey key in ephemeralKeys.Except(materialKeys)) - { - object[] idParams = key.EntityKeyValues.Select(ekv => ekv.Value).ToArray(); - object obj = await GetByIdAsync(elementType, idParams); - mmadd.Invoke(materialMany, new object[] { obj }); - } - // Remove from hasMany - if (mmremove != null) - foreach (EntityKey key in materialKeys.Except(ephemeralKeys)) - { - object[] idParams = key.EntityKeyValues.Select(ekv => ekv.Value).ToArray(); - object obj = await GetByIdAsync(elementType, idParams); - mmremove.Invoke(materialMany, new object[] { obj }); - } - } - else if(IsModel(prop.PropertyType)) - { - // A belongsTo relationship - - //if (AuthorizationModel.AllowOperation(AuthorizationOperation.Update, prop, target, newVal)) - var materialBt = prop.GetValue(material, null); - var ephemeralBt = prop.GetValue(ephemeral, null); - - var materialKey = materialBt == null ? null : MaterializeEntityKey(prop.PropertyType, materialBt); - var ephemeralKey = ephemeralBt == null ? null : MaterializeEntityKey(prop.PropertyType, ephemeralBt); - - if (materialKey != ephemeralKey) - { - object[] idParams = ephemeralKey.EntityKeyValues.Select(ekv => ekv.Value).ToArray(); - prop.SetValue(material, await GetByIdAsync(prop.PropertyType, idParams), null); - } - // else, - } - else - { - object newVal = prop.GetValue(ephemeral); - //if (AuthorizationModel.AllowOperation(AuthorizationOperation.Update, prop, target, newVal)) - if (prop.GetValue(material) != newVal) - { - prop.SetValue(material, newVal, null); - } - } - } - - } - } -} diff --git a/JSONAPI.EntityFramework/EntityFrameworkMaterializer_Util.cs b/JSONAPI.EntityFramework/EntityFrameworkMaterializer_Util.cs deleted file mode 100644 index db35bfc8..00000000 --- a/JSONAPI.EntityFramework/EntityFrameworkMaterializer_Util.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Data.Entity; -using System.Data.Entity.Core; -using System.Data.Entity.Core.Metadata.Edm; -using System.Data.Entity.Core.Objects; -using System.Data.Entity.Infrastructure; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; - -namespace JSONAPI.EntityFramework -{ - public partial class EntityFrameworkMaterializer - { - public T GetDetachedOriginal(T entity, bool fixupRelationships = false) - where T : class - { - DbPropertyValues originalValues = this.context.Entry(entity).OriginalValues; - T orig = (T)originalValues.ToObject(); - if (fixupRelationships) - { - throw new NotImplementedException(); - } - return orig; - } - - public IEnumerable GetAssociationChanges(T1 parent, string propertyName, EntityState findState) - { - ObjectContext ocontext = ((IObjectContextAdapter)this.context).ObjectContext; - MetadataWorkspace metadataWorkspace = ocontext.MetadataWorkspace; - - // Find the AssociationType that matches the property traits given as input - AssociationType atype = - metadataWorkspace.GetItems(DataSpace.CSpace) - .Where(a => a.AssociationEndMembers.Any( - ae => ae.MetadataProperties.Any(mp => mp.Name == "ClrPropertyInfo" // Magic string!!! - && ((PropertyInfo)mp.Value).Name == propertyName - && typeof(T1).IsAssignableFrom(((PropertyInfo)mp.Value).DeclaringType) - ) - ) - ).First(); - - // Find added or deleted DbDataRecords from the above discovered type - ocontext.DetectChanges(); - IEnumerable dbDataRecords = ocontext.ObjectStateManager - .GetObjectStateEntries(findState) - .Where(e => e.IsRelationship) - // Oddly, this works, while doing the same thing below requires comparing .Name...? - .Where(e => e.EntitySet.ElementType == atype) - .Select(e => findState == EntityState.Deleted ? e.OriginalValues : e.CurrentValues); - - // Get the actual entities using the EntityKeys in the DbDataRecord - IList relationChanges = new List(); - foreach (System.Data.Common.DbDataRecord ddr in dbDataRecords) - { - EntityKey ek = (EntityKey)ddr[0]; - // Comparing .ElementType to atype doesn't work, see above...? - if (!(ek.GetEntitySet(metadataWorkspace).ElementType.Name == atype.Name)) - { - ek = (EntityKey)ddr[1]; - } - relationChanges.Add((T2)ocontext.GetObjectByKey(ek)); - } - return relationChanges; - } - } -} diff --git a/JSONAPI.EntityFramework/PluralizationService.cs b/JSONAPI.EntityFramework/EntityFrameworkPluralizationService.cs similarity index 57% rename from JSONAPI.EntityFramework/PluralizationService.cs rename to JSONAPI.EntityFramework/EntityFrameworkPluralizationService.cs index 14b914e0..05739362 100644 --- a/JSONAPI.EntityFramework/PluralizationService.cs +++ b/JSONAPI.EntityFramework/EntityFrameworkPluralizationService.cs @@ -1,23 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace JSONAPI.EntityFramework -{ - public class PluralizationService : JSONAPI.Core.IPluralizationService - { - private static Lazy _pls - = new Lazy( - () => new System.Data.Entity.Infrastructure.Pluralization.EnglishPluralizationService() - ); - public string Pluralize(string s) - { - return _pls.Value.Pluralize(s); - } - public string Singularize(string s) - { - return _pls.Value.Singularize(s); - } - } -} +using System; +using JSONAPI.Core; + +namespace JSONAPI.EntityFramework +{ + /// + /// Implementation of IPluralizationService that uses EntityFramework's built-in EnglishPluralizationService + /// + public class EntityFrameworkPluralizationService : IPluralizationService + { + private static readonly Lazy _pls + = new Lazy( + () => new System.Data.Entity.Infrastructure.Pluralization.EnglishPluralizationService() + ); + public string Pluralize(string s) + { + return _pls.Value.Pluralize(s); + } + public string Singularize(string s) + { + return _pls.Value.Singularize(s); + } + } +} diff --git a/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs new file mode 100644 index 00000000..b0b26ea9 --- /dev/null +++ b/JSONAPI.EntityFramework/EntityFrameworkResourceObjectMaterializer.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Core; +using JSONAPI.Documents; +using JSONAPI.Json; + +namespace JSONAPI.EntityFramework +{ + /// + /// Default implementation of IEntityFrameworkResourceObjectMaterializer + /// + public class EntityFrameworkResourceObjectMaterializer : IEntityFrameworkResourceObjectMaterializer + { + private readonly DbContext _dbContext; + private readonly IResourceTypeRegistry _registry; + private readonly MethodInfo _openSetToManyRelationshipValueMethod; + private readonly MethodInfo _openGetExistingRecordGenericMethod; + + /// + /// Creates a new EntityFrameworkEntityFrameworkResourceObjectMaterializer + /// + /// + /// + public EntityFrameworkResourceObjectMaterializer(DbContext dbContext, IResourceTypeRegistry registry) + { + _dbContext = dbContext; + _registry = registry; + _openSetToManyRelationshipValueMethod = GetType() + .GetMethod("SetToManyRelationshipValue", BindingFlags.NonPublic | BindingFlags.Instance); + _openGetExistingRecordGenericMethod = GetType() + .GetMethod("GetExistingRecordGeneric", BindingFlags.NonPublic | BindingFlags.Instance); + } + + public async Task MaterializeResourceObject(IResourceObject resourceObject, CancellationToken cancellationToken) + { + var registration = _registry.GetRegistrationForResourceTypeName(resourceObject.Type); + + var relationshipsToInclude = new List(); + if (resourceObject.Relationships != null) + { + relationshipsToInclude.AddRange( + resourceObject.Relationships + .Select(relationshipObject => registration.GetFieldByName(relationshipObject.Key)) + .OfType()); + } + + + var material = await GetExistingRecord(registration, resourceObject.Id, relationshipsToInclude.ToArray(), cancellationToken); + if (material == null) + { + material = Activator.CreateInstance(registration.Type); + await SetIdForNewResource(resourceObject, material, registration); + _dbContext.Set(registration.Type).Add(material); + } + + await MergeFieldsIntoProperties(resourceObject, material, registration, cancellationToken); + + return material; + } + + /// + /// Allows implementers to control how a new resource's ID should be set. + /// + protected virtual Task SetIdForNewResource(IResourceObject resourceObject, object newObject, IResourceTypeRegistration typeRegistration) + { + if (resourceObject.Id != null) + { + typeRegistration.IdProperty.SetValue(newObject, Convert.ChangeType(resourceObject.Id, typeRegistration.IdProperty.PropertyType)); + } + return Task.FromResult(0); + } + + /// + /// Gets an existing record from the store by ID, if it exists + /// + /// + protected virtual async Task GetExistingRecord(IResourceTypeRegistration registration, string id, + ResourceTypeRelationship[] relationshipsToInclude, CancellationToken cancellationToken) + { + var method = _openGetExistingRecordGenericMethod.MakeGenericMethod(registration.Type); + var result = (dynamic) method.Invoke(this, new object[] {registration, id, relationshipsToInclude, cancellationToken}); // no convert needed => see GetExistingRecordGeneric => filterByIdFactory will do it + return await result; + } + + /// + /// Merges the field values of the given resource object into the materialized object + /// + /// + /// + /// + /// + /// + /// Thrown when a semantically incorrect part of the document is encountered + protected virtual async Task MergeFieldsIntoProperties(IResourceObject resourceObject, object material, + IResourceTypeRegistration registration, CancellationToken cancellationToken) + { + foreach (var attributeValue in resourceObject.Attributes) + { + var attribute = registration.GetFieldByName(attributeValue.Key) as ResourceTypeAttribute; + if (attribute == null) continue; + attribute.SetValue(material, attributeValue.Value); + } + + foreach (var relationshipValue in resourceObject.Relationships) + { + var linkage = relationshipValue.Value.Linkage; + + var typeRelationship = registration.GetFieldByName(relationshipValue.Key) as ResourceTypeRelationship; + if (typeRelationship == null) continue; + + if (typeRelationship.IsToMany) + { + if (linkage == null) + throw new DeserializationException("Missing linkage for to-many relationship", + "Expected an array for to-many linkage, but no linkage was specified.", "/data/relationships/" + relationshipValue.Key); + + if (!linkage.IsToMany) + throw new DeserializationException("Invalid linkage for to-many relationship", + "Expected an array for to-many linkage.", + "/data/relationships/" + relationshipValue.Key + "/data"); + + // TODO: One query per related object is going to be slow. At the very least, we should be able to group the queries by type + var newCollection = new List(); + foreach (var resourceIdentifier in linkage.Identifiers) + { + var relatedObjectRegistration = _registry.GetRegistrationForResourceTypeName(resourceIdentifier.Type); + var relatedObject = await GetExistingRecord(relatedObjectRegistration, resourceIdentifier.Id, null, cancellationToken); + newCollection.Add(relatedObject); + } + + var method = _openSetToManyRelationshipValueMethod.MakeGenericMethod(typeRelationship.RelatedType); + method.Invoke(this, new[] { material, newCollection, typeRelationship }); + } + else + { + if (linkage == null) + throw new DeserializationException("Missing linkage for to-one relationship", + "Expected an object for to-one linkage, but no linkage was specified.", "/data/relationships/" + relationshipValue.Key); + + if (linkage.IsToMany) + throw new DeserializationException("Invalid linkage for to-one relationship", + "Expected an object or null for to-one linkage", + "/data/relationships/" + relationshipValue.Key + "/data"); + + var identifier = linkage.Identifiers.FirstOrDefault(); + if (identifier == null) + { + typeRelationship.Property.SetValue(material, null); + } + else + { + var relatedObjectRegistration = _registry.GetRegistrationForResourceTypeName(identifier.Type); + var relatedObject = + await GetExistingRecord(relatedObjectRegistration, identifier.Id, null, cancellationToken); + + typeRelationship.Property.SetValue(material, relatedObject); + } + } + } + } + + /// + /// Gets a record by ID + /// + protected async Task GetExistingRecordGeneric(IResourceTypeRegistration registration, + string id, ResourceTypeRelationship[] relationshipsToInclude, CancellationToken cancellationToken) where TRecord : class + { + var param = Expression.Parameter(registration.Type); + var filterExpression = registration.GetFilterByIdExpression(param, id); // no conversion of id => filterByIdFactory will do it + var lambda = Expression.Lambda>(filterExpression, param); + var query = _dbContext.Set().AsQueryable() + .Where(lambda); + + if (relationshipsToInclude != null) + { + query = relationshipsToInclude.Aggregate(query, + (current, resourceTypeRelationship) => current.Include(resourceTypeRelationship.Property.Name)); + } + + return await query.FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Sets the value of a to-many relationship + /// + protected void SetToManyRelationshipValue(object material, IEnumerable relatedObjects, ResourceTypeRelationship relationship) + { + var currentValue = relationship.Property.GetValue(material); + var typedArray = relatedObjects.Select(o => (TRelated) o).ToArray(); + if (relationship.Property.PropertyType.IsAssignableFrom(typeof (List))) + { + if (currentValue == null) + { + relationship.Property.SetValue(material, typedArray.ToList()); + } + else + { + var listCurrentValue = (ICollection) currentValue; + var itemsToAdd = typedArray.Except(listCurrentValue); + var itemsToRemove = listCurrentValue.Except(typedArray).ToList(); + + foreach (var related in itemsToAdd) + listCurrentValue.Add(related); + + foreach (var related in itemsToRemove) + listCurrentValue.Remove(related); + } + } + else + { + relationship.Property.SetValue(material, typedArray); + } + } + } +} diff --git a/JSONAPI.EntityFramework/Http/ApiController.cs b/JSONAPI.EntityFramework/Http/ApiController.cs deleted file mode 100644 index c8d140c1..00000000 --- a/JSONAPI.EntityFramework/Http/ApiController.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data.Entity; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace JSONAPI.EntityFramework.Http -{ - public class ApiController : JSONAPI.Http.ApiController - where T : class // hmm...see http://stackoverflow.com/a/6451237/489116 - where TC : DbContext - { - private EntityFrameworkMaterializer _materializer = null; - - protected override JSONAPI.Core.IMaterializer MaterializerFactory() - { - if (_materializer == null) - { - DbContext context = (DbContext)Activator.CreateInstance(typeof(TC)); - _materializer = new JSONAPI.EntityFramework.EntityFrameworkMaterializer(context); - } - return _materializer; - } - - protected override TM MaterializerFactory() - { - return base.MaterializerFactory(); - } - - protected override IQueryable QueryableFactory(Core.IMaterializer materializer = null) - { - if (materializer == null) - { - materializer = MaterializerFactory(); - } - return ((EntityFrameworkMaterializer)materializer).DbContext.Set(); - } - - public override async Task> Post(IList postedObjs) - { - var materializer = this.MaterializerFactory(); - List materialList = new List(); - foreach (T postedObj in postedObjs) - { - DbContext context = materializer.DbContext; - var material = await materializer.MaterializeUpdateAsync(postedObj); - if (context.Entry(material).State == EntityState.Added) - { - await context.SaveChangesAsync(); - materialList.Add(material); - } - else - { - // POST should only create an object--if the EntityState is Unchanged or Modified, this is an illegal operation. - var e = new System.Web.Http.HttpResponseException(System.Net.HttpStatusCode.BadRequest); - //e.InnerException = new ArgumentException("The POSTed object already exists!"); // Can't do this, I guess... - throw e; - } - } - return materialList; - } - - public override async Task> Put(string id, IList putObjs) - { - var materializer = this.MaterializerFactory(); - DbContext context = materializer.DbContext; - List materialList = new List(); - foreach (T putObj in putObjs) - { - var material = await materializer.MaterializeUpdateAsync(putObj); - materialList.Add(material); - } - await context.SaveChangesAsync(); - return materialList; - } - - public override async Task Delete(string id) - { - var materializer = this.MaterializerFactory(); - DbContext context = materializer.DbContext; - T target = await materializer.GetByIdAsync(id); - context.Set().Remove(target); - await context.SaveChangesAsync(); - await base.Delete(id); - } - - protected override void Dispose(bool disposing) - { - //FIXME: Unsure what to do with the "disposing" parameter here...what does it mean?? - if (_materializer != null) - { - _materializer.DbContext.Dispose(); - } - _materializer = null; - base.Dispose(disposing); - } - } -} diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs new file mode 100644 index 00000000..114f7877 --- /dev/null +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; +using System.Linq.Expressions; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Core; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; +using JSONAPI.Extensions; +using JSONAPI.Http; + +namespace JSONAPI.EntityFramework.Http +{ + /// + /// Implementation of IDocumentMaterializer for use with Entity Framework. + /// + public class EntityFrameworkDocumentMaterializer : IDocumentMaterializer where T : class + { + protected readonly DbContext DbContext; + private readonly IResourceTypeRegistration _resourceTypeRegistration; + private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder; + private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder; + private readonly IEntityFrameworkResourceObjectMaterializer _entityFrameworkResourceObjectMaterializer; + private readonly ISortExpressionExtractor _sortExpressionExtractor; + private readonly IIncludeExpressionExtractor _includeExpressionExtractor; + private readonly IBaseUrlService _baseUrlService; + + /// + /// Creates a new EntityFrameworkDocumentMaterializer + /// + public EntityFrameworkDocumentMaterializer( + DbContext dbContext, + IResourceTypeRegistration resourceTypeRegistration, + IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, + ISingleResourceDocumentBuilder singleResourceDocumentBuilder, + IEntityFrameworkResourceObjectMaterializer entityFrameworkResourceObjectMaterializer, + ISortExpressionExtractor sortExpressionExtractor, + IIncludeExpressionExtractor includeExpressionExtractor, + IBaseUrlService baseUrlService) + { + DbContext = dbContext; + _resourceTypeRegistration = resourceTypeRegistration; + _queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder; + _singleResourceDocumentBuilder = singleResourceDocumentBuilder; + _entityFrameworkResourceObjectMaterializer = entityFrameworkResourceObjectMaterializer; + _sortExpressionExtractor = sortExpressionExtractor; + _includeExpressionExtractor = includeExpressionExtractor; + _baseUrlService = baseUrlService; + } + + public virtual Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken) + { + var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request); + var includes = _includeExpressionExtractor.ExtractIncludeExpressions(request); + var query = QueryIncludeNavigationProperties(null, GetNavigationPropertiesIncludes(includes)); + return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, sortExpressions, cancellationToken, includes); + } + + public virtual async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken) + { + var apiBaseUrl = GetBaseUrlFromRequest(request); + var includes = _includeExpressionExtractor.ExtractIncludeExpressions(request); + var singleResource = await FilterById(id, _resourceTypeRegistration, GetNavigationPropertiesIncludes(includes)).FirstOrDefaultAsync(cancellationToken); + if (singleResource == null) + throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", + _resourceTypeRegistration.ResourceTypeName, id)); + return _singleResourceDocumentBuilder.BuildDocument(singleResource, apiBaseUrl, includes, null); + } + + public virtual async Task CreateRecord(ISingleResourceDocument requestDocument, + HttpRequestMessage request, CancellationToken cancellationToken) + { + var apiBaseUrl = GetBaseUrlFromRequest(request); + var newRecord = MaterializeAsync(requestDocument.PrimaryData, cancellationToken); + await OnCreate(newRecord); + await DbContext.SaveChangesAsync(cancellationToken); + var includes = _includeExpressionExtractor.ExtractIncludeExpressions(request); + var returnDocument = _singleResourceDocumentBuilder.BuildDocument(await newRecord, apiBaseUrl, includes, null); + + return returnDocument; + } + + + public virtual async Task UpdateRecord(string id, ISingleResourceDocument requestDocument, + HttpRequestMessage request, CancellationToken cancellationToken) + { + var apiBaseUrl = GetBaseUrlFromRequest(request); + var newRecord = MaterializeAsync(requestDocument.PrimaryData, cancellationToken); + await OnUpdate(newRecord); + await DbContext.SaveChangesAsync(cancellationToken); + var includes = _includeExpressionExtractor.ExtractIncludeExpressions(request); + var returnDocument = _singleResourceDocumentBuilder.BuildDocument(await newRecord, apiBaseUrl, includes, null); + + return returnDocument; + } + + public virtual async Task DeleteRecord(string id, HttpRequestMessage request, CancellationToken cancellationToken) + { + var singleResource = DbContext.Set().FindAsync(cancellationToken, Convert.ChangeType(id, _resourceTypeRegistration.IdProperty.PropertyType)); + await OnDelete(singleResource); + DbContext.Set().Remove(await singleResource); + await DbContext.SaveChangesAsync(cancellationToken); + + return null; + } + + /// + /// Gets the base URL for link creation from the current request + /// + protected string GetBaseUrlFromRequest(HttpRequestMessage request) + { + return _baseUrlService.GetBaseUrl(request); + } + + /// + /// Convert a resource object into a material record managed by EntityFramework. + /// + /// + protected virtual async Task MaterializeAsync(IResourceObject resourceObject, CancellationToken cancellationToken) + { + return (T) await _entityFrameworkResourceObjectMaterializer.MaterializeResourceObject(resourceObject, cancellationToken); + } + + + /// + /// Manipulate entity before create. + /// + /// + /// + protected virtual async Task OnCreate(Task record) + { + await record; + } + + /// + /// Manipulate entity before update. + /// + /// + protected virtual async Task OnUpdate(Task record) + { + await record; + } + + /// + /// Manipulate entity before delete. + /// + /// + /// + protected virtual async Task OnDelete(Task record) + { + await record; + } + + /// + /// This method allows to include into query. + /// This can reduce the number of queries (eager loading) + /// + /// + /// + /// + protected virtual Expression>[] GetNavigationPropertiesIncludes(string[] includes) + { + List>> list = new List>>(); + foreach (var include in includes) + { + var incl = include.Pascalize(); + var param = Expression.Parameter(typeof(TResource)); + var lambda = + Expression.Lambda>( + Expression.PropertyOrField(param, incl),param); + list.Add(lambda); + } + return list.ToArray(); + } + + + private IQueryable QueryIncludeNavigationProperties(Expression> predicate, + params Expression>[] includes) where TResource : class + { + IQueryable query = DbContext.Set(); + if (includes != null && includes.Any()) // eager loading + query = includes.Aggregate(query, (current, include) => current.Include(include)); + + if (predicate != null) + query = query.Where(predicate); + + return query.AsQueryable(); + } + + private IQueryable FilterById(string id, IResourceTypeRegistration resourceTypeRegistration, + params Expression>[] includes) where TResource : class + { + var param = Expression.Parameter(typeof(TResource)); + var filterByIdExpression = resourceTypeRegistration.GetFilterByIdExpression(param, id); + var predicate = Expression.Lambda>(filterByIdExpression, param); + return QueryIncludeNavigationProperties(predicate, includes); + } + } +} diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs new file mode 100644 index 00000000..142075a0 --- /dev/null +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Core; +using JSONAPI.Documents.Builders; +using JSONAPI.Extensions; +using JSONAPI.Http; + +namespace JSONAPI.EntityFramework.Http +{ + /// + /// Implementation of for use with Entity Framework + /// + public class EntityFrameworkToManyRelatedResourceDocumentMaterializer : + QueryableToManyRelatedResourceDocumentMaterializer where TPrimaryResource : class + { + private readonly IResourceTypeRegistration _primaryTypeRegistration; + private readonly ResourceTypeRelationship _relationship; + private readonly DbContext _dbContext; + + /// + /// Builds a new EntityFrameworkToManyRelatedResourceDocumentMaterializer. + /// + public EntityFrameworkToManyRelatedResourceDocumentMaterializer( + ResourceTypeRelationship relationship, + DbContext dbContext, + IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder, + ISortExpressionExtractor sortExpressionExtractor, + IIncludeExpressionExtractor includeExpressionExtractor, + IResourceTypeRegistration primaryTypeRegistration) + : base(queryableResourceCollectionDocumentBuilder, sortExpressionExtractor, includeExpressionExtractor) + { + _relationship = relationship; + _dbContext = dbContext; + _primaryTypeRegistration = primaryTypeRegistration; + } + + protected override async Task> GetRelatedQuery(string primaryResourceId, + CancellationToken cancellationToken) + { + var param = Expression.Parameter(typeof (TPrimaryResource)); + var accessorExpr = Expression.Property(param, _relationship.Property); + var lambda = Expression.Lambda>>(accessorExpr, param); + + var primaryEntityQuery = FilterById(primaryResourceId, _primaryTypeRegistration); + + // We have to see if the resource even exists, so we can throw a 404 if it doesn't + var relatedResource = await primaryEntityQuery.FirstOrDefaultAsync(cancellationToken); + if (relatedResource == null) + throw JsonApiException.CreateForNotFound(string.Format( + "No resource of type `{0}` exists with id `{1}`.", + _primaryTypeRegistration.ResourceTypeName, primaryResourceId)); + var includes = GetNavigationPropertiesIncludes(Includes); + var query = primaryEntityQuery.SelectMany(lambda); + + if (includes != null && includes.Any()) + query = includes.Aggregate(query, (current, include) => current.Include(include)); + return query; + } + + + /// + /// This method allows to include into query. + /// This can reduce the number of queries (eager loading) + /// + /// + /// + protected virtual Expression>[] GetNavigationPropertiesIncludes(string[] includes) + { + List>> list = new List>>(); + foreach (var include in includes) + { + var incl = include.Pascalize(); + var param = Expression.Parameter(typeof(TRelated)); + var lambda = + Expression.Lambda>( + Expression.PropertyOrField(param, incl), param); + list.Add(lambda); + } + return list.ToArray(); + } + + private IQueryable FilterById(string id, + IResourceTypeRegistration resourceTypeRegistration) where TResource : class + { + var param = Expression.Parameter(typeof (TResource)); + var filterByIdExpression = resourceTypeRegistration.GetFilterByIdExpression(param, id); + var predicate = Expression.Lambda>(filterByIdExpression, param); + IQueryable query = _dbContext.Set(); + return query.Where(predicate); + } + + } +} diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkToOneRelatedResourceDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkToOneRelatedResourceDocumentMaterializer.cs new file mode 100644 index 00000000..7fcb3814 --- /dev/null +++ b/JSONAPI.EntityFramework/Http/EntityFrameworkToOneRelatedResourceDocumentMaterializer.cs @@ -0,0 +1,73 @@ +using System; +using System.Data.Entity; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Core; +using JSONAPI.Documents.Builders; +using JSONAPI.Http; + +namespace JSONAPI.EntityFramework.Http +{ + /// + /// Implementation of for use with Entity Framework + /// + public class EntityFrameworkToOneRelatedResourceDocumentMaterializer : + QueryableToOneRelatedResourceDocumentMaterializer where TPrimaryResource : class + { + private readonly IResourceTypeRegistration _primaryTypeRegistration; + private readonly ResourceTypeRelationship _relationship; + private readonly DbContext _dbContext; + + /// + /// Builds a new EntityFrameworkToOneRelatedResourceDocumentMaterializer + /// + public EntityFrameworkToOneRelatedResourceDocumentMaterializer( + ISingleResourceDocumentBuilder singleResourceDocumentBuilder, IBaseUrlService baseUrlService, + IResourceTypeRegistration primaryTypeRegistration, ResourceTypeRelationship relationship, + DbContext dbContext) + : base(singleResourceDocumentBuilder, baseUrlService) + { + _primaryTypeRegistration = primaryTypeRegistration; + _relationship = relationship; + _dbContext = dbContext; + } + + protected override async Task GetRelatedRecord(string primaryResourceId, CancellationToken cancellationToken) + { + var param = Expression.Parameter(typeof(TPrimaryResource)); + var accessorExpr = Expression.Property(param, _relationship.Property); + var lambda = Expression.Lambda>(accessorExpr, param); + + var primaryEntityQuery = FilterById(primaryResourceId, _primaryTypeRegistration); + var primaryEntityExists = await primaryEntityQuery.AnyAsync(cancellationToken); + if (!primaryEntityExists) + throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.", + _primaryTypeRegistration.ResourceTypeName, primaryResourceId)); + return await primaryEntityQuery.Select(lambda).FirstOrDefaultAsync(cancellationToken); + } + + private IQueryable Filter(Expression> predicate, + params Expression>[] includes) where TResource : class + { + IQueryable query = _dbContext.Set(); + if (includes != null && includes.Any()) + query = includes.Aggregate(query, (current, include) => current.Include(include)); + + if (predicate != null) + query = query.Where(predicate); + + return query.AsQueryable(); + } + + private IQueryable FilterById(string id, IResourceTypeRegistration resourceTypeRegistration, + params Expression>[] includes) where TResource : class + { + var param = Expression.Parameter(typeof(TResource)); + var filterByIdExpression = resourceTypeRegistration.GetFilterByIdExpression(param, id); + var predicate = Expression.Lambda>(filterByIdExpression, param); + return Filter(predicate, includes); + } + } +} diff --git a/JSONAPI.EntityFramework/IEntityFrameworkResourceObjectMaterializer.cs b/JSONAPI.EntityFramework/IEntityFrameworkResourceObjectMaterializer.cs new file mode 100644 index 00000000..321fd36c --- /dev/null +++ b/JSONAPI.EntityFramework/IEntityFrameworkResourceObjectMaterializer.cs @@ -0,0 +1,24 @@ +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.Documents; +using JSONAPI.Json; + +namespace JSONAPI.EntityFramework +{ + /// + /// This class manages converting IResourceObject instances from a request into records managed + /// by Entity Framework. + /// + public interface IEntityFrameworkResourceObjectMaterializer + { + /// + /// Gets a record managed by Entity Framework that has merged in the data from + /// the supplied resource object. + /// + /// + /// + /// + /// + Task MaterializeResourceObject(IResourceObject resourceObject, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj index 5953bb90..2045b33d 100644 --- a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj +++ b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj @@ -40,11 +40,11 @@ False - ..\packages\EntityFramework.6.1.2\lib\net45\EntityFramework.dll + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll False - ..\packages\EntityFramework.6.1.2\lib\net45\EntityFramework.SqlServer.dll + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll False @@ -70,12 +70,17 @@ - - - - - + + + + + + + + + + diff --git a/JSONAPI.EntityFramework/packages.config b/JSONAPI.EntityFramework/packages.config index eddb39d3..d19b6caa 100644 --- a/JSONAPI.EntityFramework/packages.config +++ b/JSONAPI.EntityFramework/packages.config @@ -1,6 +1,6 @@  - + diff --git a/JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs similarity index 70% rename from JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs rename to JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs index 6a2d6fc1..6b458e2a 100644 --- a/JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs @@ -1,21 +1,22 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Formatting; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; +using System.Threading; +using System.Web.Http; using FluentAssertions; using JSONAPI.ActionFilters; using JSONAPI.Core; -using JSONAPI.Json; +using JSONAPI.Documents.Builders; +using JSONAPI.QueryableTransformers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace JSONAPI.Tests.ActionFilters { [TestClass] - public class EnableFilteringAttributeTests + public class DefaultFilteringTransformerTests : QueryableTransformerTestsBase { private enum SomeEnum { @@ -49,22 +50,22 @@ private class Dummy public Decimal? NullableDecimalField { get; set; } public Boolean BooleanField { get; set; } public Boolean? NullableBooleanField { get; set; } - public SByte SByteField { get; set; } - public SByte? NullableSByteField { get; set; } + public SByte SbyteField { get; set; } + public SByte? NullableSbyteField { get; set; } public Byte ByteField { get; set; } public Byte? NullableByteField { get; set; } public Int16 Int16Field { get; set; } public Int16? NullableInt16Field { get; set; } - public UInt16 UInt16Field { get; set; } - public UInt16? NullableUInt16Field { get; set; } + public UInt16 Uint16Field { get; set; } + public UInt16? NullableUint16Field { get; set; } public Int32 Int32Field { get; set; } public Int32? NullableInt32Field { get; set; } - public UInt32 UInt32Field { get; set; } - public UInt32? NullableUInt32Field { get; set; } + public UInt32 Uint32Field { get; set; } + public UInt32? NullableUint32Field { get; set; } public Int64 Int64Field { get; set; } public Int64? NullableInt64Field { get; set; } - public UInt64 UInt64Field { get; set; } - public UInt64? NullableUInt64Field { get; set; } + public UInt64 Uint64Field { get; set; } + public UInt64? NullableUint64Field { get; set; } public Double DoubleField { get; set; } public Double? NullableDoubleField { get; set; } public Single SingleField { get; set; } @@ -233,12 +234,12 @@ public void SetupFixtures() new Dummy { Id = "210", - SByteField = 63 + SbyteField = 63 }, new Dummy { Id = "211", - SByteField = -89 + SbyteField = -89 }, #endregion @@ -248,7 +249,7 @@ public void SetupFixtures() new Dummy { Id = "220", - NullableSByteField = 91 + NullableSbyteField = 91 }, #endregion @@ -308,12 +309,12 @@ public void SetupFixtures() new Dummy { Id = "270", - UInt16Field = 12345 + Uint16Field = 12345 }, new Dummy { Id = "271", - UInt16Field = 45678 + Uint16Field = 45678 }, #endregion @@ -323,7 +324,7 @@ public void SetupFixtures() new Dummy { Id = "280", - NullableUInt16Field = 65000 + NullableUint16Field = 65000 }, #endregion @@ -358,12 +359,12 @@ public void SetupFixtures() new Dummy { Id = "310", - UInt32Field = 123456789 + Uint32Field = 123456789 }, new Dummy { Id = "311", - UInt32Field = 234567890 + Uint32Field = 234567890 }, #endregion @@ -373,7 +374,7 @@ public void SetupFixtures() new Dummy { Id = "320", - NullableUInt32Field = 345678901 + NullableUint32Field = 345678901 }, #endregion @@ -408,12 +409,12 @@ public void SetupFixtures() new Dummy { Id = "350", - UInt64Field = 123456789012 + Uint64Field = 123456789012 }, new Dummy { Id = "351", - UInt64Field = 234567890123 + Uint64Field = 234567890123 }, #endregion @@ -423,7 +424,7 @@ public void SetupFixtures() new Dummy { Id = "360", - NullableUInt64Field = 345678901234 + NullableUint64Field = 345678901234 }, #endregion @@ -538,54 +539,45 @@ public void SetupFixtures() _fixturesQuery = _fixtures.AsQueryable(); } - private HttpActionExecutedContext CreateActionExecutedContext(IModelManager modelManager, string uri) + private DefaultFilteringTransformer GetTransformer() { - var formatter = new JsonApiFormatter(modelManager); - - var httpContent = new ObjectContent(typeof(IQueryable), _fixturesQuery, formatter); - - return new HttpActionExecutedContext + var pluralizationService = new PluralizationService(new Dictionary { - ActionContext = new HttpActionContext - { - ControllerContext = new HttpControllerContext - { - Request = new HttpRequestMessage(HttpMethod.Get, new Uri(uri)) - } - }, - Response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = httpContent - } - }; + {"Dummy", "Dummies"} + }); + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(pluralizationService)); + var registry = new ResourceTypeRegistry(); + registry.AddRegistration(registrar.BuildRegistration(typeof(Dummy))); + registry.AddRegistration(registrar.BuildRegistration(typeof(RelatedItemWithId))); + return new DefaultFilteringTransformer(registry); } - private T[] GetArray(string uri) + private Dummy[] GetArray(string uri) { - var modelManager = new ModelManager(new PluralizationService()); - - var filter = new EnableFilteringAttribute(modelManager); - - var context = CreateActionExecutedContext(modelManager, uri); - - filter.OnActionExecuted(context); - - var returnedContent = context.Response.Content as ObjectContent; - returnedContent.Should().NotBeNull(); - returnedContent.ObjectType.Should().Be(typeof(IQueryable)); + return Transform(GetTransformer(), _fixturesQuery, uri).ToArray(); + } - var returnedQueryable = returnedContent.Value as IQueryable; - returnedQueryable.Should().NotBeNull(); + #region String - return returnedQueryable.ToArray(); + [TestMethod] + public void Filters_by_matching_string_id_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[id]=100"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("100"); } - #region String + [TestMethod] + public void Filters_by_missing_string_id_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[id]="); + returnedArray.Length.Should().Be(0); + } [TestMethod] public void Filters_by_matching_string_property() { - var returnedArray = GetArray("http://api.example.com/dummies?stringField=String value 1"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=String value 1"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("100"); } @@ -593,7 +585,7 @@ public void Filters_by_matching_string_property() [TestMethod] public void Filters_by_missing_string_property() { - var returnedArray = GetArray("http://api.example.com/dummies?stringField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 3); returnedArray.Any(d => d.Id == "100" || d.Id == "101" || d.Id == "102").Should().BeFalse(); } @@ -605,7 +597,7 @@ public void Filters_by_missing_string_property() [TestMethod] public void Filters_by_matching_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeField=1930-11-07"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[date-time-field]=1930-11-07"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("110"); } @@ -613,14 +605,14 @@ public void Filters_by_matching_datetime_property() [TestMethod] public void Filters_by_missing_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[date-time-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeField=1961-02-18"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]=1961-02-18"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("120"); } @@ -628,7 +620,7 @@ public void Filters_by_matching_nullable_datetime_property() [TestMethod] public void Filters_by_missing_nullable_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "120").Should().BeFalse(); } @@ -640,7 +632,7 @@ public void Filters_by_missing_nullable_datetime_property() [TestMethod] public void Filters_by_matching_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeOffsetField=1991-01-03"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[date-time-offset-field]=1991-01-03"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("130"); } @@ -648,14 +640,14 @@ public void Filters_by_matching_datetimeoffset_property() [TestMethod] public void Filters_by_missing_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeOffsetField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[date-time-offset-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeOffsetField=2014-05-05"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-offset-field]=2014-05-05"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("140"); } @@ -663,7 +655,7 @@ public void Filters_by_matching_nullable_datetimeoffset_property() [TestMethod] public void Filters_by_missing_nullable_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeOffsetField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-offset-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "140").Should().BeFalse(); } @@ -675,7 +667,7 @@ public void Filters_by_missing_nullable_datetimeoffset_property() [TestMethod] public void Filters_by_matching_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?enumField=1"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[enum-field]=1"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("150"); } @@ -683,14 +675,14 @@ public void Filters_by_matching_enum_property() [TestMethod] public void Filters_by_missing_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?enumField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[enum-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableEnumField=3"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-enum-field]=3"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("160"); } @@ -698,7 +690,7 @@ public void Filters_by_matching_nullable_enum_property() [TestMethod] public void Filters_by_missing_nullable_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableEnumField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-enum-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "160").Should().BeFalse(); } @@ -710,22 +702,35 @@ public void Filters_by_missing_nullable_enum_property() [TestMethod] public void Filters_by_matching_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?decimalField=4.03"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[decimal-field]=4.03"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("170"); } + [TestMethod] + public void Filters_by_matching_decimal_property_non_en_US() + { + var currentCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = new CultureInfo("se-SE"); + + var returnedArray = GetArray("http://api.example.com/dummies?filter[decimal-field]=4.03"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("170"); + + Thread.CurrentThread.CurrentCulture = currentCulture; + } + [TestMethod] public void Filters_by_missing_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?decimalField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[decimal-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDecimalField=12.09"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-decimal-field]=12.09"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("180"); } @@ -733,7 +738,7 @@ public void Filters_by_matching_nullable_decimal_property() [TestMethod] public void Filters_by_missing_nullable_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDecimalField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-decimal-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "180").Should().BeFalse(); } @@ -745,7 +750,7 @@ public void Filters_by_missing_nullable_decimal_property() [TestMethod] public void Filters_by_matching_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?booleanField=true"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[boolean-field]=true"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("190"); } @@ -753,14 +758,14 @@ public void Filters_by_matching_boolean_property() [TestMethod] public void Filters_by_missing_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?booleanField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[boolean-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableBooleanField=false"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-boolean-field]=false"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("200"); } @@ -768,7 +773,7 @@ public void Filters_by_matching_nullable_boolean_property() [TestMethod] public void Filters_by_missing_nullable_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableBooleanField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-boolean-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "200").Should().BeFalse(); } @@ -780,7 +785,7 @@ public void Filters_by_missing_nullable_boolean_property() [TestMethod] public void Filters_by_matching_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?sByteField=63"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[sbyte-field]=63"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("210"); } @@ -788,14 +793,14 @@ public void Filters_by_matching_sbyte_property() [TestMethod] public void Filters_by_missing_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?sByteField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[sbyte-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableSByteField=91"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-sbyte-field]=91"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("220"); } @@ -803,7 +808,7 @@ public void Filters_by_matching_nullable_sbyte_property() [TestMethod] public void Filters_by_missing_nullable_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableSByteField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-sbyte-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "220").Should().BeFalse(); } @@ -815,7 +820,7 @@ public void Filters_by_missing_nullable_sbyte_property() [TestMethod] public void Filters_by_matching_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?byteField=250"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[byte-field]=250"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("230"); } @@ -823,14 +828,14 @@ public void Filters_by_matching_byte_property() [TestMethod] public void Filters_by_missing_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?byteField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[byte-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableByteField=44"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-byte-field]=44"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("240"); } @@ -838,7 +843,7 @@ public void Filters_by_matching_nullable_byte_property() [TestMethod] public void Filters_by_missing_nullable_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableByteField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-byte-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "240").Should().BeFalse(); } @@ -850,7 +855,7 @@ public void Filters_by_missing_nullable_byte_property() [TestMethod] public void Filters_by_matching_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int16Field=12345"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int16-field]=12345"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("250"); } @@ -858,14 +863,14 @@ public void Filters_by_matching_int16_property() [TestMethod] public void Filters_by_missing_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int16Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int16-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt16Field=32764"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-int16-field]=32764"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("260"); } @@ -873,7 +878,7 @@ public void Filters_by_matching_nullable_int16_property() [TestMethod] public void Filters_by_missing_nullable_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt16Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-int16-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "260").Should().BeFalse(); } @@ -885,7 +890,7 @@ public void Filters_by_missing_nullable_int16_property() [TestMethod] public void Filters_by_matching_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt16Field=12345"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uint16-field]=12345"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("270"); } @@ -893,14 +898,14 @@ public void Filters_by_matching_uint16_property() [TestMethod] public void Filters_by_missing_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt16Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uint16-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt16Field=65000"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-uint16-field]=65000"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("280"); } @@ -908,7 +913,7 @@ public void Filters_by_matching_nullable_uint16_property() [TestMethod] public void Filters_by_missing_nullable_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt16Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-uint16-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "280").Should().BeFalse(); } @@ -920,7 +925,7 @@ public void Filters_by_missing_nullable_uint16_property() [TestMethod] public void Filters_by_matching_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int32Field=100000006"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int32-field]=100000006"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("290"); } @@ -928,14 +933,14 @@ public void Filters_by_matching_int32_property() [TestMethod] public void Filters_by_missing_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int32Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int32-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt32Field=345678901"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-int32-field]=345678901"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("300"); } @@ -943,7 +948,7 @@ public void Filters_by_matching_nullable_int32_property() [TestMethod] public void Filters_by_missing_nullable_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt32Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-int32-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "300").Should().BeFalse(); } @@ -955,7 +960,7 @@ public void Filters_by_missing_nullable_int32_property() [TestMethod] public void Filters_by_matching_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt32Field=123456789"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uint32-field]=123456789"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("310"); } @@ -963,14 +968,14 @@ public void Filters_by_matching_uint32_property() [TestMethod] public void Filters_by_missing_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt32Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uint32-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt32Field=345678901"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-uint32-field]=345678901"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("320"); } @@ -978,7 +983,7 @@ public void Filters_by_matching_nullable_uint32_property() [TestMethod] public void Filters_by_missing_nullable_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt32Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-uint32-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "320").Should().BeFalse(); } @@ -990,7 +995,7 @@ public void Filters_by_missing_nullable_uint32_property() [TestMethod] public void Filters_by_matching_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int64Field=123453489012"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int64-field]=123453489012"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("330"); } @@ -998,14 +1003,14 @@ public void Filters_by_matching_int64_property() [TestMethod] public void Filters_by_missing_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int64Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[int64-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt64Field=345671901234"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-int64-field]=345671901234"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("340"); } @@ -1013,7 +1018,7 @@ public void Filters_by_matching_nullable_int64_property() [TestMethod] public void Filters_by_missing_nullable_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt64Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-int64-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "340").Should().BeFalse(); } @@ -1025,7 +1030,7 @@ public void Filters_by_missing_nullable_int64_property() [TestMethod] public void Filters_by_matching_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt64Field=123456789012"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uint64-field]=123456789012"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("350"); } @@ -1033,14 +1038,14 @@ public void Filters_by_matching_uint64_property() [TestMethod] public void Filters_by_missing_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt64Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[uint64-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt64Field=345678901234"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-uint64-field]=345678901234"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("360"); } @@ -1048,7 +1053,7 @@ public void Filters_by_matching_nullable_uint64_property() [TestMethod] public void Filters_by_missing_nullable_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt64Field="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-uint64-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "360").Should().BeFalse(); } @@ -1060,22 +1065,36 @@ public void Filters_by_missing_nullable_uint64_property() [TestMethod] public void Filters_by_matching_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?singleField=21.56901"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[single-field]=21.56901"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("370"); + } + + [TestMethod] + public void Filters_by_matching_single_property_non_en_US() + { + var currentCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = new CultureInfo("se-SE"); + + var returnedArray = GetArray("http://api.example.com/dummies?filter[single-field]=21.56901"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("370"); + + Thread.CurrentThread.CurrentCulture = currentCulture; } + [TestMethod] public void Filters_by_missing_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?singleField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[single-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableSingleField=1.3456"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-single-field]=1.3456"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("380"); } @@ -1083,7 +1102,7 @@ public void Filters_by_matching_nullable_single_property() [TestMethod] public void Filters_by_missing_nullable_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableSingleField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-single-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "380").Should().BeFalse(); } @@ -1095,22 +1114,35 @@ public void Filters_by_missing_nullable_single_property() [TestMethod] public void Filters_by_matching_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?doubleField=12.3453489012"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[double-field]=12.3453489012"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("390"); + } + + [TestMethod] + public void Filters_by_matching_double_property_non_en_US() + { + var currentCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = new CultureInfo("se-SE"); + + var returnedArray = GetArray("http://api.example.com/dummies?filter[double-field]=12.3453489012"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("390"); + + Thread.CurrentThread.CurrentCulture = currentCulture; } [TestMethod] public void Filters_by_missing_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?doubleField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[double-field]="); returnedArray.Length.Should().Be(0); } [TestMethod] public void Filters_by_matching_nullable_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDoubleField=34567.1901234"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-double-field]=34567.1901234"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("400"); } @@ -1118,7 +1150,7 @@ public void Filters_by_matching_nullable_double_property() [TestMethod] public void Filters_by_missing_nullable_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDoubleField="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-double-field]="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "400").Should().BeFalse(); } @@ -1130,8 +1162,8 @@ public void Filters_by_missing_nullable_double_property() [TestMethod] public void Does_not_filter_unknown_type() { - var returnedArray = GetArray("http://api.example.com/dummies?unknownTypeField=asdfasd"); - returnedArray.Length.Should().Be(_fixtures.Count); + Action action = () => GetArray("http://api.example.com/dummies?filter[unknownTypeField]=asdfasd"); + action.ShouldThrow().Which.Error.Status.Should().Be(HttpStatusCode.BadRequest); } #endregion @@ -1141,7 +1173,7 @@ public void Does_not_filter_unknown_type() [TestMethod] public void Filters_by_matching_to_one_relationship_id() { - var returnedArray = GetArray("http://api.example.com/dummies?toOneRelatedItem=1101"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[to-one-related-item]=1101"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("1100"); } @@ -1149,7 +1181,7 @@ public void Filters_by_matching_to_one_relationship_id() [TestMethod] public void Filters_by_missing_to_one_relationship_id() { - var returnedArray = GetArray("http://api.example.com/dummies?toOneRelatedItem="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[to-one-related-item]="); returnedArray.Length.Should().Be(_fixtures.Count - 2); returnedArray.Any(d => d.Id == "1100" || d.Id == "1102").Should().BeFalse(); } @@ -1161,7 +1193,7 @@ public void Filters_by_missing_to_one_relationship_id() [TestMethod] public void Filters_by_matching_id_in_to_many_relationship() { - var returnedArray = GetArray("http://api.example.com/dummies?toManyRelatedItems=1111"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[to-many-related-items]=1111"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("1110"); } @@ -1169,7 +1201,7 @@ public void Filters_by_matching_id_in_to_many_relationship() [TestMethod] public void Filters_by_missing_id_in_to_many_relationship() { - var returnedArray = GetArray("http://api.example.com/dummies?toManyRelatedItems="); + var returnedArray = GetArray("http://api.example.com/dummies?filter[to-many-related-items]="); returnedArray.Length.Should().Be(_fixtures.Count - 2); returnedArray.Any(d => d.Id == "1110" || d.Id == "1120").Should().BeFalse(); } @@ -1181,7 +1213,7 @@ public void Filters_by_missing_id_in_to_many_relationship() [TestMethod] public void Ands_together_filters() { - var returnedArray = GetArray("http://api.example.com/dummies?stringField=String value 2&enumField=3"); + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=String value 2&filter[enum-field]=3"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("102"); } diff --git a/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs new file mode 100644 index 00000000..8cb1e0af --- /dev/null +++ b/JSONAPI.Tests/ActionFilters/DefaultPaginationTransformerTests.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using FluentAssertions; +using JSONAPI.ActionFilters; +using JSONAPI.Core; +using JSONAPI.Documents.Builders; +using JSONAPI.QueryableTransformers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Tests.ActionFilters +{ + [TestClass] + public class DefaultPaginationTransformerTests + { + private class Dummy + { + // ReSharper disable UnusedAutoPropertyAccessor.Local + public string Id { get; set; } + // ReSharper restore UnusedAutoPropertyAccessor.Local + + public string FirstName { get; set; } + + public string LastName { get; set; } + } + + private IList _fixtures; + private IQueryable _fixturesQuery; + + [TestInitialize] + public void SetupFixtures() + { + _fixtures = new List + { + new Dummy { Id = "1", FirstName = "Thomas", LastName = "Paine" }, + new Dummy { Id = "2", FirstName = "Samuel", LastName = "Adams" }, + new Dummy { Id = "3", FirstName = "George", LastName = "Washington"}, + new Dummy { Id = "4", FirstName = "Thomas", LastName = "Jefferson" }, + new Dummy { Id = "5", FirstName = "Martha", LastName = "Washington"}, + new Dummy { Id = "6", FirstName = "Abraham", LastName = "Lincoln" }, + new Dummy { Id = "7", FirstName = "Andrew", LastName = "Jackson" }, + new Dummy { Id = "8", FirstName = "Andrew", LastName = "Johnson" }, + new Dummy { Id = "9", FirstName = "William", LastName = "Harrison" } + }; + _fixturesQuery = _fixtures.AsQueryable(); + } + + private DefaultPaginationTransformer GetTransformer(int maxPageSize) + { + return new DefaultPaginationTransformer(maxPageSize); + } + + private Dummy[] GetArray(string uri, int maxPageSize = 50) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + return GetTransformer(maxPageSize).ApplyPagination(_fixturesQuery, request).PagedQuery.ToArray(); + } + + [TestMethod] + public void ApplyPagination_has_no_effect_when_no_paging_parameters_are_supplied() + { + var array = GetArray("http://api.example.com/dummies"); + array.Length.Should().Be(9); + } + + [TestMethod] + public void ApplyPagination_returns_all_results_when_they_are_within_page() + { + var array = GetArray("http://api.example.com/dummies?page[number]=0&page[size]=10"); + array.Length.Should().Be(9); + } + + [TestMethod] + public void ApplyPagination_returns_first_page_of_data() + { + var array = GetArray("http://api.example.com/dummies?page[number]=0&page[size]=4"); + array.Should().BeEquivalentTo(_fixtures[0], _fixtures[1], _fixtures[2], _fixtures[3]); + } + + [TestMethod] + public void ApplyPagination_returns_second_page_of_data() + { + var array = GetArray("http://api.example.com/dummies?page[number]=1&page[size]=4"); + array.Should().BeEquivalentTo(_fixtures[4], _fixtures[5], _fixtures[6], _fixtures[7]); + } + + [TestMethod] + public void ApplyPagination_returns_page_at_end() + { + var array = GetArray("http://api.example.com/dummies?page[number]=2&page[size]=4"); + array.Should().BeEquivalentTo(_fixtures[8]); + } + + [TestMethod] + public void ApplyPagination_returns_nothing_for_page_after_end() + { + var array = GetArray("http://api.example.com/dummies?page[number]=3&page[size]=4"); + array.Length.Should().Be(0); + } + + [TestMethod] + public void ApplyPagination_uses_max_page_size_when_requested_page_size_is_higher() + { + var array = GetArray("http://api.example.com/dummies?page[number]=1&page[size]=8", 3); + array.Should().BeEquivalentTo(_fixtures[3], _fixtures[4], _fixtures[5]); + } + + [TestMethod] + public void ApplyPagination_throws_exception_if_page_number_is_negative() + { + Action action = () => + { + GetArray("http://api.example.com/dummies?page[number]=-4&page[size]=4"); + }; + action.ShouldThrow().And.Error.Detail.Should().Be("Page number must not be negative."); + } + + [TestMethod] + public void ApplyPagination_throws_exception_if_page_size_is_negative() + { + Action action = () => + { + GetArray("http://api.example.com/dummies?page[number]=0&page[size]=-4"); + }; + action.ShouldThrow().And.Error.Detail.Should().Be("Page size must be greater than or equal to 1."); + } + + [TestMethod] + public void ApplyPagination_throws_exception_when_page_size_is_zero() + { + Action action = () => + { + GetArray("http://api.example.com/dummies?page[number]=0&page[size]=0"); + }; + action.ShouldThrow().And.Error.Detail.Should().Be("Page size must be greater than or equal to 1."); + } + + [TestMethod] + public void ApplyPagination_throws_exception_if_page_number_specified_but_not_size() + { + Action action = () => + { + GetArray("http://api.example.com/dummies?page[number]=0"); + }; + action.ShouldThrow().And.Error.Detail.Should().Be("In order for paging to work properly, if either page.number or page.size is set, both must be."); + } + + [TestMethod] + public void ApplyPagination_throws_exception_if_page_size_specified_but_not_number() + { + Action action = () => + { + GetArray("http://api.example.com/dummies?page[size]=0"); + }; + action.ShouldThrow().And.Error.Detail.Should().Be("In order for paging to work properly, if either page.number or page.size is set, both must be."); + } + + [TestMethod] + public void DefaultPaginationTransformer_cannot_be_instantiated_if_max_page_size_is_zero() + { + Action action = () => + { + GetTransformer(0); + }; + action.ShouldThrow(); + } + + [TestMethod] + public void DefaultPaginationTransformer_cannot_be_instantiated_if_max_page_size_is_negative() + { + Action action = () => + { + GetTransformer(-4); + }; + action.ShouldThrow(); + } + } +} diff --git a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs new file mode 100644 index 00000000..4283b1eb --- /dev/null +++ b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using JSONAPI.Core; +using JSONAPI.Documents.Builders; +using JSONAPI.QueryableTransformers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Tests.ActionFilters +{ + [TestClass] + public class DefaultSortingTransformerTests : QueryableTransformerTestsBase + { + private class Dummy + { + // ReSharper disable UnusedAutoPropertyAccessor.Local + public string Id { get; set; } + // ReSharper restore UnusedAutoPropertyAccessor.Local + + public string FirstName { get; set; } + + public string LastName { get; set; } + + public DateTime BirthDate { get; set; } + } + + private class Dummy2 + { + public int Id { get; set; } + + public string Name { get; set; } + } + + private IList _fixtures; + private IQueryable _fixturesQuery; + private IList _fixtures2; + private IQueryable _fixtures2Query; + + [TestInitialize] + public void SetupFixtures() + { + _fixtures = new List + { + new Dummy {Id = "1", FirstName = "Thomas", LastName = "Paine", BirthDate = new DateTime(1737, 2, 9)}, + new Dummy {Id = "2", FirstName = "Samuel", LastName = "Adams", BirthDate = new DateTime(1722, 9, 27)}, + new Dummy {Id = "3", FirstName = "George", LastName = "Washington", BirthDate = new DateTime(1732, 2, 22)}, + new Dummy {Id = "4", FirstName = "Thomas", LastName = "Jefferson", BirthDate = new DateTime(1743, 4, 13)}, + new Dummy {Id = "5", FirstName = "Martha", LastName = "Washington", BirthDate = new DateTime(1731, 6, 13)}, + new Dummy {Id = "6", FirstName = "Abraham", LastName = "Lincoln", BirthDate = new DateTime(1809, 2, 12)}, + new Dummy {Id = "7", FirstName = "Andrew", LastName = "Jackson", BirthDate = new DateTime(1767, 3, 15)}, + new Dummy {Id = "8", FirstName = "Andrew", LastName = "Johnson", BirthDate = new DateTime(1808, 12, 29)}, + new Dummy {Id = "9", FirstName = "William", LastName = "Harrison", BirthDate = new DateTime(1773, 2, 9)} + }; + _fixturesQuery = _fixtures.AsQueryable(); + + _fixtures2 = new List + { + new Dummy2 {Id = 45, Name = "France"}, + new Dummy2 {Id = 52, Name = "Spain"}, + new Dummy2 {Id = 33, Name = "Mongolia"}, + }; + _fixtures2Query = _fixtures2.AsQueryable(); + } + + private DefaultSortingTransformer GetTransformer() + { + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + var registry = new ResourceTypeRegistry(); + registry.AddRegistration(registrar.BuildRegistration(typeof(Dummy), "dummies")); + registry.AddRegistration(registrar.BuildRegistration(typeof(Dummy2), "dummy2s")); + return new DefaultSortingTransformer(registry); + } + + private TFixture[] GetArray(string[] sortExpressions, IQueryable fixturesQuery) + { + return GetTransformer().Sort(fixturesQuery, sortExpressions).ToArray(); + } + + private Dummy[] GetDummyArray(string[] sortExpressions) + { + return GetArray(sortExpressions, _fixturesQuery); + } + + private Dummy2[] GetDummy2Array(string[] sortExpressions) + { + return GetArray(sortExpressions, _fixtures2Query); + } + + private void RunTransformAndExpectFailure(string[] sortExpressions, string expectedMessage) + { + Action action = () => + { + // ReSharper disable once UnusedVariable + var result = GetTransformer().Sort(_fixturesQuery, sortExpressions).ToArray(); + }; + action.ShouldThrow().Which.Error.Detail.Should().Be(expectedMessage); + } + + [TestMethod] + public void Sorts_by_attribute_ascending() + { + var array = GetDummyArray(new [] { "first-name" }); + array.Should().BeInAscendingOrder(d => d.FirstName); + } + + [TestMethod] + public void Sorts_by_attribute_descending() + { + var array = GetDummyArray(new [] { "-first-name" }); + array.Should().BeInDescendingOrder(d => d.FirstName); + } + + [TestMethod] + public void Sorts_by_two_ascending_attributes() + { + var array = GetDummyArray(new [] { "last-name", "first-name" }); + array.Should().ContainInOrder(_fixtures.OrderBy(d => d.LastName + d.FirstName)); + } + + [TestMethod] + public void Sorts_by_two_descending_attributes() + { + var array = GetDummyArray(new [] { "-last-name", "-first-name" }); + array.Should().ContainInOrder(_fixtures.OrderByDescending(d => d.LastName + d.FirstName)); + } + + [TestMethod] + public void Sorts_by_id_when_expressions_are_empty() + { + var array = GetDummyArray(new string[] { }); + array.Should().ContainInOrder(_fixtures.OrderBy(d => d.Id)); + } + + [TestMethod] + public void Returns_400_if_sort_argument_is_empty() + { + RunTransformAndExpectFailure(new[] { "" }, "One of the sort expressions is empty."); + } + + [TestMethod] + public void Returns_400_if_sort_argument_is_whitespace() + { + RunTransformAndExpectFailure(new [] { " " }, "One of the sort expressions is empty."); + } + + [TestMethod] + public void Returns_400_if_sort_argument_is_empty_descending() + { + RunTransformAndExpectFailure(new [] { "-" }, "One of the sort expressions is empty."); + } + + [TestMethod] + public void Returns_400_if_sort_argument_is_whitespace_descending() + { + RunTransformAndExpectFailure(new[] { "- " }, "One of the sort expressions is empty."); + } + + [TestMethod] + public void Returns_400_if_no_property_exists() + { + RunTransformAndExpectFailure(new[] { "foobar" }, + "The attribute \"foobar\" does not exist on type \"dummies\"."); + } + + [TestMethod] + public void Returns_400_if_the_same_property_is_specified_more_than_once() + { + RunTransformAndExpectFailure(new[] { "last-name", "last-name" }, + "The attribute \"last-name\" was specified more than once."); + } + + [TestMethod] + public void Can_sort_by_DateTimeOffset() + { + var array = GetDummyArray(new [] { "birth-date" }); + array.Should().BeInAscendingOrder(d => d.BirthDate); + } + + [TestMethod] + public void Can_sort_by_resource_with_integer_key() + { + var array = GetDummy2Array(new [] { "name" }); + array.Should().BeInAscendingOrder(d => d.Name); + } + } +} diff --git a/JSONAPI.Tests/ActionFilters/FallbackDocumentBuilderAttributeTests.cs b/JSONAPI.Tests/ActionFilters/FallbackDocumentBuilderAttributeTests.cs new file mode 100644 index 00000000..49a87216 --- /dev/null +++ b/JSONAPI.Tests/ActionFilters/FallbackDocumentBuilderAttributeTests.cs @@ -0,0 +1,173 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Formatting; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; +using FluentAssertions; +using JSONAPI.ActionFilters; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace JSONAPI.Tests.ActionFilters +{ + [TestClass] + public class FallbackDocumentBuilderAttributeTests + { + private HttpActionExecutedContext GetActionExecutedContext(object objectContentValue, Exception exception = null) + { + var mockMediaTypeFormatter = new Mock(MockBehavior.Strict); + mockMediaTypeFormatter.Setup(f => f.CanWriteType(It.IsAny())).Returns(true); + mockMediaTypeFormatter.Setup(f => f.SetDefaultContentHeaders(It.IsAny(), It.IsAny(), It.IsAny())); + var response = new HttpResponseMessage + { + Content = new ObjectContent(objectContentValue.GetType(), objectContentValue, mockMediaTypeFormatter.Object) + }; + var actionContext = new HttpActionContext { Response = response }; + return new HttpActionExecutedContext(actionContext, exception); + } + + [TestMethod] + public void OnActionExecutedAsync_leaves_ISingleResourceDocument_alone() + { + // Arrange + var mockDocument = new Mock(MockBehavior.Strict); + var actionExecutedContext = GetActionExecutedContext(mockDocument.Object); + var cancellationTokenSource = new CancellationTokenSource(); + var mockFallbackDocumentBuilder = new Mock(MockBehavior.Strict); + var mockErrorDocumentBuilder = new Mock(MockBehavior.Strict); + + // Act + var attribute = new FallbackDocumentBuilderAttribute(mockFallbackDocumentBuilder.Object, mockErrorDocumentBuilder.Object); + var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); + task.Wait(); + + ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockDocument.Object); + actionExecutedContext.Response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + [TestMethod] + public void OnActionExecutedAsync_leaves_IResourceCollectionDocument_alone() + { + // Arrange + var mockDocument = new Mock(MockBehavior.Strict); + var actionExecutedContext = GetActionExecutedContext(mockDocument.Object); + var cancellationTokenSource = new CancellationTokenSource(); + var mockFallbackDocumentBuilder = new Mock(MockBehavior.Strict); + var mockErrorDocumentBuilder = new Mock(MockBehavior.Strict); + + // Act + var attribute = new FallbackDocumentBuilderAttribute(mockFallbackDocumentBuilder.Object, mockErrorDocumentBuilder.Object); + var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); + task.Wait(); + + ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockDocument.Object); + actionExecutedContext.Response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + [TestMethod] + public void OnActionExecutedAsync_leaves_IErrorDocument_alone_but_changes_request_status_to_match_error_status() + { + // Arrange + var mockError = new Mock(MockBehavior.Strict); + mockError.Setup(e => e.Status).Returns(HttpStatusCode.Conflict); + var mockDocument = new Mock(MockBehavior.Strict); + mockDocument.Setup(p => p.Errors).Returns(new[] {mockError.Object}); + var actionExecutedContext = GetActionExecutedContext(mockDocument.Object); + var cancellationTokenSource = new CancellationTokenSource(); + var mockFallbackDocumentBuilder = new Mock(MockBehavior.Strict); + var mockErrorDocumentBuilder = new Mock(MockBehavior.Strict); + + // Act + var attribute = new FallbackDocumentBuilderAttribute(mockFallbackDocumentBuilder.Object, mockErrorDocumentBuilder.Object); + var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); + task.Wait(); + + ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockDocument.Object); + actionExecutedContext.Response.StatusCode.Should().Be(HttpStatusCode.Conflict); + } + + [TestMethod] + public void OnActionExecutedAsync_does_nothing_if_there_is_an_exception() + { + // Arrange + var objectContent = new object(); + var theException = new Exception("This is an error."); + var actionExecutedContext = GetActionExecutedContext(objectContent, theException); + var cancellationTokenSource = new CancellationTokenSource(); + var mockFallbackDocumentBuilder = new Mock(MockBehavior.Strict); + var mockErrorDocumentBuilder = new Mock(MockBehavior.Strict); + + // Act + var attribute = new FallbackDocumentBuilderAttribute(mockFallbackDocumentBuilder.Object, mockErrorDocumentBuilder.Object); + var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); + task.Wait(); + + var newObjectContent = ((ObjectContent) actionExecutedContext.Response.Content).Value; + newObjectContent.Should().BeSameAs(objectContent); + actionExecutedContext.Exception.Should().Be(theException); + } + + private class Fruit + { + } + + [TestMethod] + public void OnActionExecutedAsync_delegates_to_fallback_document_builder_for_unknown_types() + { + // Arrange + var resource = new Fruit(); + var actionExecutedContext = GetActionExecutedContext(resource); + var cancellationTokenSource = new CancellationTokenSource(); + + var mockResult = new Mock(MockBehavior.Strict); + var mockFallbackDocumentBuilder = new Mock(MockBehavior.Strict); + mockFallbackDocumentBuilder.Setup(b => b.BuildDocument(resource, It.IsAny(), cancellationTokenSource.Token)) + .Returns(Task.FromResult(mockResult.Object)); + + var mockErrorDocumentBuilder = new Mock(MockBehavior.Strict); + + // Act + var attribute = new FallbackDocumentBuilderAttribute(mockFallbackDocumentBuilder.Object, mockErrorDocumentBuilder.Object); + var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); + task.Wait(); + + // Assert + ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockResult.Object); + actionExecutedContext.Response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + [TestMethod] + public void OnActionExecutedAsync_creates_IErrorDocument_for_HttpError() + { + // Arrange + var httpError = new HttpError("Some error"); + var actionExecutedContext = GetActionExecutedContext(httpError); + var cancellationTokenSource = new CancellationTokenSource(); + var mockFallbackDocumentBuilder = new Mock(MockBehavior.Strict); + + var mockError = new Mock(MockBehavior.Strict); + mockError.Setup(e => e.Status).Returns(HttpStatusCode.OK); + var mockResult = new Mock(MockBehavior.Strict); + mockResult.Setup(r => r.Errors).Returns(new[] { mockError.Object }); + + var mockErrorDocumentBuilder = new Mock(MockBehavior.Strict); + mockErrorDocumentBuilder.Setup(b => b.BuildFromHttpError(httpError, HttpStatusCode.OK)).Returns(mockResult.Object); + + // Act + var attribute = new FallbackDocumentBuilderAttribute(mockFallbackDocumentBuilder.Object, mockErrorDocumentBuilder.Object); + var task = attribute.OnActionExecutedAsync(actionExecutedContext, cancellationTokenSource.Token); + task.Wait(); + + // Assert + ((ObjectContent)actionExecutedContext.Response.Content).Value.Should().BeSameAs(mockResult.Object); + actionExecutedContext.Response.StatusCode.Should().Be(HttpStatusCode.OK); + } + } +} diff --git a/JSONAPI.Tests/ActionFilters/QueryableTransformerTestsBase.cs b/JSONAPI.Tests/ActionFilters/QueryableTransformerTestsBase.cs new file mode 100644 index 00000000..adba3c0a --- /dev/null +++ b/JSONAPI.Tests/ActionFilters/QueryableTransformerTestsBase.cs @@ -0,0 +1,16 @@ +using System.Linq; +using System.Net.Http; +using JSONAPI.ActionFilters; +using JSONAPI.QueryableTransformers; + +namespace JSONAPI.Tests.ActionFilters +{ + public abstract class QueryableTransformerTestsBase + { + internal IQueryable Transform(IQueryableFilteringTransformer filteringTransformer, IQueryable query, string uri) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + return filteringTransformer.Filter(query, request); + } + } +} diff --git a/JSONAPI.Tests/Core/DefaultNamingConventionsTests.cs b/JSONAPI.Tests/Core/DefaultNamingConventionsTests.cs new file mode 100644 index 00000000..5ce99dcc --- /dev/null +++ b/JSONAPI.Tests/Core/DefaultNamingConventionsTests.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.Attributes; +using JSONAPI.Core; +using JSONAPI.Tests.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; + +namespace JSONAPI.Tests.Core +{ + [TestClass] + public class DefaultNamingConventionsTests + { + private class Band + { + [JsonProperty("THE-GENRE")] + public string Genre { get; set; } + } + + private class SomeClass + { + public string SomeKey { get; set; } + } + + [TestMethod] + public void GetFieldNameForProperty_returns_right_name_for_id() + { + // Arrange + var namingConventions = new DefaultNamingConventions(new PluralizationService()); + + // Act + var name = namingConventions.GetFieldNameForProperty(typeof(Author).GetProperty("Id")); + + // Assert + name.Should().Be("id"); + } + + [TestMethod] + public void GetFieldNameForProperty_returns_right_name_for_camel_cased_property() + { + // Arrange + var namingConventions = new DefaultNamingConventions(new PluralizationService()); + + // Act + var name = namingConventions.GetFieldNameForProperty(typeof(SomeClass).GetProperty("SomeKey")); + + // Assert + name.Should().Be("some-key"); + } + + [TestMethod] + public void GetFieldNameForProperty_returns_right_name_for_property_with_JsonProperty_attribute() + { + // Arrange + var namingConventions = new DefaultNamingConventions(new PluralizationService()); + + // Act + var name = namingConventions.GetFieldNameForProperty(typeof(Band).GetProperty("Genre")); + + // Assert + name.Should().Be("THE-GENRE"); + } + } +} diff --git a/JSONAPI.Tests/Core/EnumAttributeValueConverterTests.cs b/JSONAPI.Tests/Core/EnumAttributeValueConverterTests.cs new file mode 100644 index 00000000..9f14283f --- /dev/null +++ b/JSONAPI.Tests/Core/EnumAttributeValueConverterTests.cs @@ -0,0 +1,71 @@ +using FluentAssertions; +using JSONAPI.Core; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Tests.Core +{ + [TestClass] + public class EnumAttributeValueConverterTests + { + public enum Int32Enum + { + Value1 = 1, + Value2 = 2, + Value3 = 3 + } + + public enum Int64Enum : long + { + Value1 = 1, + Value2 = 2, + Value3 = 3 + } + + private class Class1 + { + public Int32Enum Value { get; set; } + } + + private class Class2 + { + public Int64Enum Value { get; set; } + } + + [TestMethod] + public void GetValue_for_int32_enum() + { + // Arrange + var property = typeof (Class1).GetProperty("Value"); + var obj = new Class1 + { + Value = Int32Enum.Value1 + }; + + // Act + var converter = new EnumAttributeValueConverter(property, typeof(Int32Enum), false); + var actualValue = (JValue)converter.GetValue(obj); + + // Assert + actualValue.Value.Should().Be((long)1); + } + + [TestMethod] + public void GetValue_for_int64_enum() + { + // Arrange + var property = typeof(Class2).GetProperty("Value"); + var obj = new Class2 + { + Value = Int64Enum.Value1 + }; + + // Act + var converter = new EnumAttributeValueConverter(property, typeof(Int64Enum), false); + var actualValue = (JValue)converter.GetValue(obj); + + // Assert + actualValue.Value.Should().Be((long)1); + } + } +} diff --git a/JSONAPI.Tests/Core/MetadataManagerTests.cs b/JSONAPI.Tests/Core/MetadataManagerTests.cs deleted file mode 100644 index e4a0d259..00000000 --- a/JSONAPI.Tests/Core/MetadataManagerTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using JSONAPI.Json; -using System.IO; -using JSONAPI.Tests.Models; -using JSONAPI.Core; - -namespace JSONAPI.Tests.Core -{ - [TestClass] - public class MetadataManagerTests - { - [TestMethod] - public void PropertyWasPresentTest() - { - // Arrange - - JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(new JSONAPI.Core.PluralizationService()); - MemoryStream stream = new MemoryStream(); - - stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(@"{""posts"":{""id"":42,""links"":{""author"":""18""}}}")); - - Post p; - p = (Post)formatter.ReadFromStreamAsync(typeof(Post), stream, (System.Net.Http.HttpContent)null, (System.Net.Http.Formatting.IFormatterLogger)null).Result; - - // Act - bool idWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Id")); - bool titleWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Title")); - bool authorWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Author")); - bool commentsWasSet = MetadataManager.Instance.PropertyWasPresent(p, p.GetType().GetProperty("Comments")); - - // Assert - Assert.IsTrue(idWasSet, "Id was not reported as set, but was."); - Assert.IsFalse(titleWasSet, "Title was reported as set, but was not."); - Assert.IsTrue(authorWasSet, "Author was not reported as set, but was."); - Assert.IsFalse(commentsWasSet, "Comments was reported as set, but was not."); - } - } -} diff --git a/JSONAPI.Tests/Core/ModelManagerTests.cs b/JSONAPI.Tests/Core/ModelManagerTests.cs deleted file mode 100644 index 3773454b..00000000 --- a/JSONAPI.Tests/Core/ModelManagerTests.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using JSONAPI.Core; -using JSONAPI.Tests.Models; -using System.Reflection; -using System.Collections.Generic; -using System.Collections; - -namespace JSONAPI.Tests.Core -{ - [TestClass] - public class ModelManagerTests - { - private class InvalidModel // No Id discernable! - { - public string Data { get; set; } - } - - private class CustomIdModel - { - [JSONAPI.Attributes.UseAsId] - public Guid Uuid { get; set; } - - public string Data { get; set; } - } - - [TestMethod] - public void FindsIdNamedId() - { - // Arrange - var mm = new ModelManager(new PluralizationService()); - - // Act - PropertyInfo idprop = mm.GetIdProperty(typeof(Author)); - - // Assert - Assert.AreSame(typeof(Author).GetProperty("Id"), idprop); - } - - [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] - public void DoesntFindMissingId() - { - // Arrange - var mm = new ModelManager(new PluralizationService()); - - // Act - PropertyInfo idprop = mm.GetIdProperty(typeof(InvalidModel)); - - // Assert - Assert.Fail("An InvalidOperationException should be thrown and we shouldn't get here!"); - } - - [TestMethod] - public void FindsIdFromAttribute() - { - // Arrange - var mm = new ModelManager(new PluralizationService()); - - // Act - PropertyInfo idprop = mm.GetIdProperty(typeof(CustomIdModel)); - // Assert - Assert.AreSame(typeof(CustomIdModel).GetProperty("Uuid"), idprop); - } - - [TestMethod] - public void GetJsonKeyForTypeTest() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - - // Act - var postKey = mm.GetJsonKeyForType(typeof(Post)); - var authorKey = mm.GetJsonKeyForType(typeof(Author)); - var commentKey = mm.GetJsonKeyForType(typeof(Comment)); - var manyCommentKey = mm.GetJsonKeyForType(typeof(Comment[])); - - // Assert - Assert.AreEqual("posts", postKey); - Assert.AreEqual("authors", authorKey); - Assert.AreEqual("comments", commentKey); - Assert.AreEqual("comments", manyCommentKey); - } - - [TestMethod] - public void GetJsonKeyForPropertyTest() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - - // Act - var idKey = mm.GetJsonKeyForProperty(typeof(Author).GetProperty("Id")); - var nameKey = mm.GetJsonKeyForProperty(typeof(Author).GetProperty("Name")); - var postsKey = mm.GetJsonKeyForProperty(typeof(Author).GetProperty("Posts")); - - // Assert - Assert.AreEqual("id", idKey); - Assert.AreEqual("name", nameKey); - Assert.AreEqual("posts", postsKey); - - } - - [TestMethod] - public void GetPropertyForJsonKeyTest() - { - // Arrange - var pluralizationService = new PluralizationService(); - var mm = new ModelManager(pluralizationService); - Type authorType = typeof(Author); - - // Act - var idProp = mm.GetPropertyForJsonKey(authorType, "id"); - var nameProp = mm.GetPropertyForJsonKey(authorType, "name"); - var postsProp = mm.GetPropertyForJsonKey(authorType, "posts"); - - // Assert - Assert.AreSame(authorType.GetProperty("Id"), idProp); - Assert.AreSame(authorType.GetProperty("Name"), nameProp); - Assert.AreSame(authorType.GetProperty("Posts"), postsProp); - - } - - [TestMethod] - public void IsSerializedAsManyTest() - { - // Arrange - var mm = new ModelManager(new PluralizationService()); - - // Act - bool isArray = mm.IsSerializedAsMany(typeof(Post[])); - bool isGenericEnumerable = mm.IsSerializedAsMany(typeof(IEnumerable)); - bool isString = mm.IsSerializedAsMany(typeof(string)); - bool isAuthor = mm.IsSerializedAsMany(typeof(Author)); - bool isNonGenericEnumerable = mm.IsSerializedAsMany(typeof(IEnumerable)); - - // Assert - Assert.IsTrue(isArray); - Assert.IsTrue(isGenericEnumerable); - Assert.IsFalse(isString); - Assert.IsFalse(isAuthor); - Assert.IsFalse(isNonGenericEnumerable); - } - - [TestMethod] - public void GetElementTypeTest() - { - // Arrange - var mm = new ModelManager(new PluralizationService()); - - // Act - Type postTypeFromArray = mm.GetElementType(typeof(Post[])); - Type postTypeFromEnumerable = mm.GetElementType(typeof(IEnumerable)); - - // Assert - Assert.AreSame(typeof(Post), postTypeFromArray); - Assert.AreSame(typeof(Post), postTypeFromEnumerable); - } - - [TestMethod] - public void GetElementTypeInvalidArgumentTest() - { - // Arrange - var mm = new ModelManager(new PluralizationService()); - - // Act - Type x = mm.GetElementType(typeof(Author)); - - // Assert - Assert.IsNull(x, "Return value of GetElementType should be null for a non-Many type argument!"); - } - } -} diff --git a/JSONAPI.Tests/Core/PrimitiveTypeAttributeValueConverterTests.cs b/JSONAPI.Tests/Core/PrimitiveTypeAttributeValueConverterTests.cs new file mode 100644 index 00000000..eb7b53f2 --- /dev/null +++ b/JSONAPI.Tests/Core/PrimitiveTypeAttributeValueConverterTests.cs @@ -0,0 +1,52 @@ +using FluentAssertions; +using JSONAPI.Core; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Tests.Core +{ + [TestClass] + public class PrimitiveTypeAttributeValueConverterTests + { + private class Class1 + { + public int? NullableIntValue { get; set; } + } + + [TestMethod] + public void GetValue_for_null() + { + // Arrange + var property = typeof (Class1).GetProperty("NullableIntValue"); + var obj = new Class1 + { + NullableIntValue = null + }; + + // Act + var converter = new PrimitiveTypeAttributeValueConverter(property); + var actualValue = (JValue)converter.GetValue(obj); + + // Assert + ((object)actualValue).Should().Be(null); + } + + [TestMethod] + public void SetValue_for_null() + { + // Arrange + var property = typeof(Class1).GetProperty("NullableIntValue"); + var obj = new Class1 + { + NullableIntValue = 4 + }; + + // Act + var converter = new PrimitiveTypeAttributeValueConverter(property); + converter.SetValue(obj, JValue.CreateNull()); + + // Assert + obj.NullableIntValue.Should().Be(null); + } + } +} diff --git a/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs b/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs new file mode 100644 index 00000000..b1b242e9 --- /dev/null +++ b/JSONAPI.Tests/Core/ResourceTypeRegistrarTests.cs @@ -0,0 +1,813 @@ +using System; +using JSONAPI.Attributes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using JSONAPI.Core; +using JSONAPI.Tests.Models; +using System.Reflection; +using System.Collections.Generic; +using System.Collections; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Threading; +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Tests.Core +{ + [TestClass] + public class ResourceTypeRegistrarTests + { + private class InvalidModel // No Id discernable! + { + public string Data { get; set; } + } + + private class CustomIdModel + { + [UseAsId] + public Guid Uuid { get; set; } + + public string Data { get; set; } + } + + private class Salad + { + public string Id { get; set; } + + [JsonProperty("salad-type")] + public string TheSaladType { get; set; } + + [JsonProperty("salad-type")] + public string AnotherSaladType { get; set; } + } + + private class Continent + { + [UseAsId] + public string Name { get; set; } + + public string Id { get; set; } + } + + private class Boat + { + public string Id { get; set; } + + public string Type { get; set; } + } + + [TestMethod] + public void Cant_register_type_with_missing_id() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + Action action = () => registrar.BuildRegistration(typeof(InvalidModel)); + + // Assert + action.ShouldThrow() + .Which.Message.Should() + .Be("Unable to determine Id property for type `InvalidModel`."); + } + + [TestMethod] + public void Cant_register_type_with_non_id_property_called_id() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + Action action = () => registrar.BuildRegistration(typeof(Continent)); + + // Assert + action.ShouldThrow() + .Which.Message.Should() + .Be("Failed to register type `Continent` because it contains a non-id property that would serialize as \"id\"."); + } + + [TestMethod] + public void Cant_register_type_with_property_called_type() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + Action action = () => registrar.BuildRegistration(typeof(Boat)); + + // Assert + action.ShouldThrow() + .Which.Message.Should() + .Be("Failed to register type `Boat` because it contains a property that would serialize as \"type\"."); + } + + [TestMethod] + public void Cant_register_type_with_two_properties_with_the_same_name() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + Type saladType = typeof(Salad); + + // Act + Action action = () => registrar.BuildRegistration(saladType); + + // Assert + action.ShouldThrow().Which.Message.Should() + .Be("Failed to register type `Salad` because contains multiple properties that would serialize as `salad-type`."); + } + + [TestMethod] + public void BuildRegistration_sets_up_registration_correctly() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var postReg = registrar.BuildRegistration(typeof(Post)); + + // Assert + postReg.IdProperty.Should().BeSameAs(typeof(Post).GetProperty("Id")); + postReg.ResourceTypeName.Should().Be("posts"); + postReg.Attributes.Length.Should().Be(1); + postReg.Attributes.First().Property.Should().BeSameAs(typeof(Post).GetProperty("Title")); + postReg.Relationships.Length.Should().Be(2); + postReg.Relationships[0].IsToMany.Should().BeFalse(); + postReg.Relationships[0].Property.Should().BeSameAs(typeof(Post).GetProperty("Author")); + postReg.Relationships[0].SelfLinkTemplate.Should().BeNull(); + postReg.Relationships[0].RelatedResourceLinkTemplate.Should().BeNull(); + postReg.Relationships[1].IsToMany.Should().BeTrue(); + postReg.Relationships[1].Property.Should().BeSameAs(typeof(Post).GetProperty("Comments")); + postReg.Relationships[1].SelfLinkTemplate.Should().Be("/posts/{1}/relationships/comments"); + postReg.Relationships[1].RelatedResourceLinkTemplate.Should().Be("/posts/{1}/comments"); + } + + private AttributeGrabBag InitializeGrabBag() + { + return new AttributeGrabBag() + { + Id = "2", + BooleanField = true, + NullableBooleanField = true, + SbyteField = 123, + NullableSbyteField = 123, + ByteField = 253, + NullableByteField = 253, + Int16Field = 32000, + NullableInt16Field = 32000, + Uint16Field = 64000, + NullableUint16Field = 64000, + Int32Field = 2000000000, + NullableInt32Field = 2000000000, + Uint32Field = 3000000000, + NullableUint32Field = 3000000000, + Int64Field = 9223372036854775807, + NullableInt64Field = 9223372036854775807, + Uint64Field = 9223372036854775808, + NullableUint64Field = 9223372036854775808, + DoubleField = 1056789.123, + NullableDoubleField = 1056789.123, + SingleField = 1056789.123f, + NullableSingleField = 1056789.123f, + DecimalField = 1056789.123m, + NullableDecimalField = 1056789.123m, + DateTimeField = new DateTime(1776, 07, 04), + NullableDateTimeField = new DateTime(1776, 07, 04), + DateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), + NullableDateTimeOffsetField = new DateTimeOffset(new DateTime(1776, 07, 04), new TimeSpan(-5, 0, 0)), + GuidField = new Guid("6566F9B4-5245-40DE-890D-98B40A4AD656"), + NullableGuidField = new Guid("3D1FB81E-43EE-4D04-AF91-C8A326341293"), + StringField = "Some string 156", + EnumField = SampleEnum.Value1, + NullableEnumField = SampleEnum.Value2, + ComplexAttributeField = "{\"foo\": { \"baz\": [11] }, \"bar\": 5}" + }; + } + + private void AssertAttribute(IResourceTypeRegistration reg, string attributeName, + JToken tokenToSet, TPropertyType expectedPropertyValue, TTokenType expectedTokenAfterSet, Func getPropertyFunc) + { + AssertAttributeHelper(reg, attributeName, tokenToSet, grabBag => + { + var propertyValueAfterSet = getPropertyFunc(grabBag); + propertyValueAfterSet.Should().Be(expectedPropertyValue); + }, token => + { + if (expectedTokenAfterSet == null) + token.Should().BeNull(); + else + { + var convertedTokenValue = token.Value(); + convertedTokenValue.Should().Be(expectedTokenAfterSet); + } + }); + } + + private void AssertAttributeHelper(IResourceTypeRegistration reg, string attributeName, + JToken tokenToSet, Action testPropertyValueAfterSet, + Action testTokenAfterSetAndGet) + { + var grabBag = InitializeGrabBag(); + + var field = reg.GetFieldByName(attributeName); + var attribute = (ResourceTypeAttribute)field; + attribute.JsonKey.Should().Be(attributeName); + + attribute.SetValue(grabBag, tokenToSet); + testPropertyValueAfterSet(grabBag); + + var convertedToken = attribute.GetValue(grabBag); + testTokenAfterSetAndGet(convertedToken); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_boolean_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "boolean-field", false, false, false, g => g.BooleanField); + AssertAttribute(reg, "boolean-field", true, true, true, g => g.BooleanField); + AssertAttribute(reg, "boolean-field", null, false, false, g => g.BooleanField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_boolean_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-boolean-field", false, false, false, g => g.NullableBooleanField); + AssertAttribute(reg, "nullable-boolean-field", true, true, true, g => g.NullableBooleanField); + AssertAttribute(reg, "nullable-boolean-field", null, null, (Boolean?) null, g => g.NullableBooleanField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_SByte_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "sbyte-field", 0, 0, 0, g => g.SbyteField); + AssertAttribute(reg, "sbyte-field", 12, 12, 12, g => g.SbyteField); + AssertAttribute(reg, "sbyte-field", -12, -12, -12, g => g.SbyteField); + AssertAttribute(reg, "sbyte-field", null, 0, 0, g => g.SbyteField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_SByte_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-sbyte-field", 0, (SByte?)0, (SByte?)0, g => g.NullableSbyteField); + AssertAttribute(reg, "nullable-sbyte-field", 12, (SByte?)12, (SByte?)12, g => g.NullableSbyteField); + AssertAttribute(reg, "nullable-sbyte-field", -12, (SByte?)-12, (SByte?)-12, g => g.NullableSbyteField); + AssertAttribute(reg, "nullable-sbyte-field", null, null, (SByte?)null, g => g.NullableSbyteField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Byte_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "byte-field", 0, 0, 0, g => g.ByteField); + AssertAttribute(reg, "byte-field", 12, 12, 12, g => g.ByteField); + AssertAttribute(reg, "byte-field", null, 0, 0, g => g.ByteField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_Byte_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-byte-field", 0, (Byte?)0, (Byte?)0, g => g.NullableByteField); + AssertAttribute(reg, "nullable-byte-field", 12, (Byte?)12, (Byte?)12, g => g.NullableByteField); + AssertAttribute(reg, "nullable-byte-field", null, null, (Byte?)null, g => g.NullableByteField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Int16_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "int16-field", 0, 0, 0, g => g.Int16Field); + AssertAttribute(reg, "int16-field", 4000, 4000, 4000, g => g.Int16Field); + AssertAttribute(reg, "int16-field", -4000, -4000, -4000, g => g.Int16Field); + AssertAttribute(reg, "int16-field", null, 0, 0, g => g.Int16Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_Int16_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-int16-field", 0, (Int16?)0, (Int16?)0, g => g.NullableInt16Field); + AssertAttribute(reg, "nullable-int16-field", 4000, (Int16?)4000, (Int16?)4000, g => g.NullableInt16Field); + AssertAttribute(reg, "nullable-int16-field", -4000, (Int16?)-4000, (Int16?)-4000, g => g.NullableInt16Field); + AssertAttribute(reg, "nullable-int16-field", null, null, (Int16?)null, g => g.NullableInt16Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_UInt16_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "uint16-field", 0, 0, 0, g => g.Uint16Field); + AssertAttribute(reg, "uint16-field", 4000, 4000, 4000, g => g.Uint16Field); + AssertAttribute(reg, "uint16-field", null, 0, 0, g => g.Uint16Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_UInt16_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-uint16-field", 0, (UInt16?)0, (UInt16?)0, g => g.NullableUint16Field); + AssertAttribute(reg, "nullable-uint16-field", 4000, (UInt16?)4000, (UInt16?)4000, g => g.NullableUint16Field); + AssertAttribute(reg, "nullable-uint16-field", null, null, (UInt16?)null, g => g.NullableUint16Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Int32_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "int32-field", 0, 0, 0, g => g.Int32Field); + AssertAttribute(reg, "int32-field", 2000000, 2000000, 2000000, g => g.Int32Field); + AssertAttribute(reg, "int32-field", -2000000, -2000000, -2000000, g => g.Int32Field); + AssertAttribute(reg, "int32-field", null, 0, 0, g => g.Int32Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_Int32_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-int32-field", 0, 0, (Int32?)0, g => g.NullableInt32Field); + AssertAttribute(reg, "nullable-int32-field", 2000000, 2000000, (Int32?)2000000, g => g.NullableInt32Field); + AssertAttribute(reg, "nullable-int32-field", -2000000, -2000000, (Int32?)-2000000, g => g.NullableInt32Field); + AssertAttribute(reg, "nullable-int32-field", null, null, (Int32?)null, g => g.NullableInt32Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_UInt32_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "uint32-field", 0, (UInt32)0, (UInt32)0, g => g.Uint32Field); + AssertAttribute(reg, "uint32-field", 2000000, (UInt32)2000000, (UInt32)2000000, g => g.Uint32Field); + AssertAttribute(reg, "uint32-field", null, (UInt32)0, (UInt32)0, g => g.Uint32Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_UInt32_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-uint32-field", 0, (UInt32?)0, (UInt32?)0, g => g.NullableUint32Field); + AssertAttribute(reg, "nullable-uint32-field", 2000000, (UInt32?)2000000, (UInt32?)2000000, g => g.NullableUint32Field); + AssertAttribute(reg, "nullable-uint32-field", null, null, (UInt32?)null, g => g.NullableUint32Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Int64_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "int64-field", 0, 0, 0, g => g.Int64Field); + AssertAttribute(reg, "int64-field", 20000000000, 20000000000, 20000000000, g => g.Int64Field); + AssertAttribute(reg, "int64-field", -20000000000, -20000000000, -20000000000, g => g.Int64Field); + AssertAttribute(reg, "int64-field", null, 0, 0, g => g.Int64Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_Int64_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-int64-field", 0, 0, (Int64?)0, g => g.NullableInt64Field); + AssertAttribute(reg, "nullable-int64-field", 20000000000, 20000000000, (Int64?)20000000000, g => g.NullableInt64Field); + AssertAttribute(reg, "nullable-int64-field", -20000000000, -20000000000, (Int64?)-20000000000, g => g.NullableInt64Field); + AssertAttribute(reg, "nullable-int64-field", null, null, (Int64?)null, g => g.NullableInt64Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_UInt64_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "uint64-field", 0, (UInt64)0, (UInt64)0, g => g.Uint64Field); + AssertAttribute(reg, "uint64-field", 20000000000, (UInt64)20000000000, (UInt64)20000000000, g => g.Uint64Field); + AssertAttribute(reg, "uint64-field", null, (UInt64)0, (UInt64)0, g => g.Uint64Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_UInt64_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-uint64-field", 0, (UInt64?)0, (UInt64?)0, g => g.NullableUint64Field); + AssertAttribute(reg, "nullable-uint64-field", 20000000000, (UInt64?)20000000000, (UInt64?)20000000000, g => g.NullableUint64Field); + AssertAttribute(reg, "nullable-uint64-field", null, null, (UInt64?)null, g => g.NullableUint64Field); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Single_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "single-field", 0f, 0f, 0f, g => g.SingleField); + AssertAttribute(reg, "single-field", 20000000000.1234f, 20000000000.1234f, 20000000000.1234f, g => g.SingleField); + AssertAttribute(reg, "single-field", -20000000000.1234f, -20000000000.1234f, -20000000000.1234f, g => g.SingleField); + AssertAttribute(reg, "single-field", null, 0, 0, g => g.SingleField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_Single_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-single-field", 0f, 0f, 0f, g => g.NullableSingleField); + AssertAttribute(reg, "nullable-single-field", 20000000000.1234f, 20000000000.1234f, (Int64?)20000000000.1234f, g => g.NullableSingleField); + AssertAttribute(reg, "nullable-single-field", -20000000000.1234f, -20000000000.1234f, -20000000000.1234f, g => g.NullableSingleField); + AssertAttribute(reg, "nullable-single-field", null, null, (Single?)null, g => g.NullableSingleField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Double_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "double-field", 0d, 0d, 0d, g => g.DoubleField); + AssertAttribute(reg, "double-field", 20000000000.1234d, 20000000000.1234d, 20000000000.1234d, g => g.DoubleField); + AssertAttribute(reg, "double-field", null, 0d, 0d, g => g.DoubleField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_Double_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-double-field", 0d, 0d, 0d, g => g.NullableDoubleField); + AssertAttribute(reg, "nullable-double-field", 20000000000.1234d, 20000000000.1234d, 20000000000.1234d, g => g.NullableDoubleField); + AssertAttribute(reg, "nullable-double-field", null, null, (Double?)null, g => g.NullableDoubleField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Decimal_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "decimal-field", "0", 0m, "0", g => g.DecimalField); + AssertAttribute(reg, "decimal-field", "20000000000.1234", 20000000000.1234m, "20000000000.1234", g => g.DecimalField); + AssertAttribute(reg, "decimal-field", null, 0m, "0", g => g.DecimalField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_Decimal_field_non_en_US() + { + // Set up non US culture + var culture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = new CultureInfo("se-SE"); + + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + AssertAttribute(reg, "decimal-field", "20000000000.1234", 20000000000.1234m, "20000000000.1234", g => g.DecimalField); + + Thread.CurrentThread.CurrentCulture = culture; + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_Decimal_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-decimal-field", "0", 0m, "0", g => g.NullableDecimalField); + AssertAttribute(reg, "nullable-decimal-field", "20000000000.1234", 20000000000.1234m, "20000000000.1234", g => g.NullableDecimalField); + AssertAttribute(reg, "nullable-decimal-field", null, null, (string)null, g => g.NullableDecimalField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_guid_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + var guid = new Guid("6566f9b4-5245-40de-890d-98b40a4ad656"); + AssertAttribute(reg, "guid-field", "6566f9b4-5245-40de-890d-98b40a4ad656", guid, "6566f9b4-5245-40de-890d-98b40a4ad656", g => g.GuidField); + AssertAttribute(reg, "guid-field", null, new Guid(), "00000000-0000-0000-0000-000000000000", g => g.GuidField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_guid_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + var guid = new Guid("6566f9b4-5245-40de-890d-98b40a4ad656"); + AssertAttribute(reg, "nullable-guid-field", "6566f9b4-5245-40de-890d-98b40a4ad656", guid, "6566f9b4-5245-40de-890d-98b40a4ad656", g => g.NullableGuidField); + AssertAttribute(reg, "nullable-guid-field", null, null, (Guid?)null, g => g.NullableGuidField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_DateTime_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "date-time-field", "1776-07-04", new DateTime(1776, 07, 04, 0, 0, 0, DateTimeKind.Utc), "1776-07-04T00:00:00", g => g.DateTimeField); + AssertAttribute(reg, "date-time-field", "1776-07-04T00:00:00", new DateTime(1776, 07, 04, 0, 0, 0, DateTimeKind.Utc), "1776-07-04T00:00:00", g => g.DateTimeField); + AssertAttribute(reg, "date-time-field", null, new DateTime(), "0001-01-01T00:00:00", g => g.DateTimeField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_DateTime_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-date-time-field", "1776-07-04", new DateTime(1776, 07, 04, 0, 0, 0, DateTimeKind.Utc), "1776-07-04T00:00:00", g => g.NullableDateTimeField); + AssertAttribute(reg, "nullable-date-time-field", "1776-07-04T00:00:00", new DateTime(1776, 07, 04, 0, 0, 0, DateTimeKind.Utc), "1776-07-04T00:00:00", g => g.NullableDateTimeField); + AssertAttribute(reg, "nullable-date-time-field", null, null, (DateTime?)null, g => g.NullableDateTimeField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_DateTimeOffset_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + var testDateTimeOffset1 = new DateTimeOffset(new DateTime(1776, 07, 04), TimeSpan.FromHours(-5)); + var testDateTimeOffset2 = new DateTimeOffset(new DateTime(1776, 07, 04, 12, 30, 0), TimeSpan.FromHours(0)); + var testDateTimeOffset3 = new DateTimeOffset(new DateTime(2015, 03, 11, 04, 31, 0), TimeSpan.FromHours(0)); + AssertAttribute(reg, "date-time-offset-field", "1776-07-04T00:00:00-05:00", testDateTimeOffset1, "1776-07-04T00:00:00.0000000-05:00", g => g.DateTimeOffsetField); + AssertAttribute(reg, "date-time-offset-field", "1776-07-04T12:30:00+00:00", testDateTimeOffset2, "1776-07-04T12:30:00.0000000+00:00", g => g.DateTimeOffsetField); + AssertAttribute(reg, "date-time-offset-field", "2015-03-11T04:31:00.0000000+00:00", testDateTimeOffset3, "2015-03-11T04:31:00.0000000+00:00", g => g.DateTimeOffsetField); + AssertAttribute(reg, "date-time-offset-field", null, new DateTimeOffset(), "0001-01-01T00:00:00.0000000+00:00", g => g.DateTimeOffsetField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_DateTimeOffset_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + var testDateTimeOffset = new DateTimeOffset(new DateTime(1776, 07, 04), TimeSpan.FromHours(-5)); + AssertAttribute(reg, "nullable-date-time-offset-field", "1776-07-04T00:00:00-05:00", testDateTimeOffset, "1776-07-04T00:00:00.0000000-05:00", + g => g.NullableDateTimeOffsetField); + AssertAttribute(reg, "nullable-date-time-offset-field", null, null, (DateTimeOffset?)null, + g => g.NullableDateTimeOffsetField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_string_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "string-field", "asdf", "asdf", "asdf", g => g.StringField); + AssertAttribute(reg, "string-field", null, null, (string)null, g => g.StringField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_enum_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "enum-field", (int)SampleEnum.Value1, SampleEnum.Value1, (int)SampleEnum.Value1, g => g.EnumField); + AssertAttribute(reg, "enum-field", null, (SampleEnum)0, 0, g => g.EnumField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_nullable_enum_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttribute(reg, "nullable-enum-field", (int)SampleEnum.Value1, SampleEnum.Value1, (int)SampleEnum.Value1, g => g.NullableEnumField); + AssertAttribute(reg, "nullable-enum-field", null, null, (SampleEnum?)null, g => g.NullableEnumField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_to_one_complex_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttributeHelper(reg, "to-one-complex-type-field", + new JObject { { "intProp", 32 }, { "StringProp", "qux" } }, + grabBag => + { + grabBag.ToOneComplexTypeField.Should().NotBeNull(); + grabBag.ToOneComplexTypeField.IntProp.Should().Be(32); + grabBag.ToOneComplexTypeField.StringProp.Should().Be("qux"); + }, + token => + { + ((int)token["intProp"]).Should().Be(32); + ((string)token["StringProp"]).Should().Be("qux"); + }); + AssertAttribute(reg, "to-one-complex-type-field", null, null, (SampleComplexType)null, g => g.ToOneComplexTypeField); + } + + [TestMethod] + public void BuildRegistration_sets_up_correct_attribute_for_to_many_complex_field() + { + // Arrange + var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(new PluralizationService())); + + // Act + var reg = registrar.BuildRegistration(typeof(AttributeGrabBag)); + + // Assert + AssertAttributeHelper(reg, "to-many-complex-type-field", + new JArray + { + new JObject { { "intProp", 49 }, { "StringProp", "blue" } }, + new JObject { { "intProp", 67 }, { "StringProp", "orange" } } + }, + grabBag => + { + var result = grabBag.ToManyComplexTypeField.ToArray(); + result.Length.Should().Be(2); + result[0].IntProp.Should().Be(49); + result[0].StringProp.Should().Be("blue"); + result[1].IntProp.Should().Be(67); + result[1].StringProp.Should().Be("orange"); + }, + token => + { + var jarray = (JArray) token; + jarray.Count.Should().Be(2); + ((int)jarray[0]["intProp"]).Should().Be(49); + ((string)jarray[0]["StringProp"]).Should().Be("blue"); + ((int)jarray[1]["intProp"]).Should().Be(67); + ((string)jarray[1]["StringProp"]).Should().Be("orange"); + }); + AssertAttribute(reg, "to-many-complex-type-field", null, null, (SampleComplexType[])null, g => g.ToManyComplexTypeField); + } + } +} diff --git a/JSONAPI.Tests/Core/ResourceTypeRegistryTests.cs b/JSONAPI.Tests/Core/ResourceTypeRegistryTests.cs new file mode 100644 index 00000000..7ffdb336 --- /dev/null +++ b/JSONAPI.Tests/Core/ResourceTypeRegistryTests.cs @@ -0,0 +1,167 @@ +using System; +using FluentAssertions; +using JSONAPI.Core; +using JSONAPI.Tests.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace JSONAPI.Tests.Core +{ + [TestClass] + public class ResourceTypeRegistryTests + { + private class DerivedPost : Post + { + + } + + [TestMethod] + public void GetRegistrationForType_returns_correct_value_for_registered_types() + { + // Arrange + var mockPostRegistration = new Mock(MockBehavior.Strict); + mockPostRegistration.Setup(m => m.Type).Returns(typeof(Post)); + mockPostRegistration.Setup(m => m.ResourceTypeName).Returns("posts"); + + var mockAuthorRegistration = new Mock(MockBehavior.Strict); + mockAuthorRegistration.Setup(m => m.Type).Returns(typeof(Author)); + mockAuthorRegistration.Setup(m => m.ResourceTypeName).Returns("authors"); + + var registry = new ResourceTypeRegistry(); + registry.AddRegistration(mockPostRegistration.Object); + registry.AddRegistration(mockAuthorRegistration.Object); + + // Act + var authorReg = registry.GetRegistrationForType(typeof(Author)); + var postReg = registry.GetRegistrationForType(typeof(Post)); + + // Assert + postReg.Should().BeSameAs(mockPostRegistration.Object); + authorReg.Should().BeSameAs(mockAuthorRegistration.Object); + } + + [TestMethod] + public void GetRegistrationForType_gets_registration_for_closest_registered_base_type_for_unregistered_type() + { + // Arrange + var mockPostRegistration = new Mock(MockBehavior.Strict); + mockPostRegistration.Setup(m => m.Type).Returns(typeof(Post)); + mockPostRegistration.Setup(m => m.ResourceTypeName).Returns("posts"); + + var registry = new ResourceTypeRegistry(); + registry.AddRegistration(mockPostRegistration.Object); + + // Act + var registration = registry.GetRegistrationForType(typeof(DerivedPost)); + + // Assert + registration.Type.Should().Be(typeof(Post)); + } + + [TestMethod] + public void GetRegistrationForType_fails_when_getting_unregistered_type() + { + // Arrange + var registry = new ResourceTypeRegistry(); + + // Act + Action action = () => + { + registry.GetRegistrationForType(typeof(Post)); + }; + + // Assert + action.ShouldThrow().WithMessage("No type registration was found for the type \"Post\"."); + } + + [TestMethod] + public void GetRegistrationForResourceTypeName_fails_when_getting_unregistered_type_name() + { + // Arrange + var registry = new ResourceTypeRegistry(); + + // Act + Action action = () => + { + registry.GetRegistrationForResourceTypeName("posts"); + }; + + // Assert + action.ShouldThrow().WithMessage("No type registration was found for the type name \"posts\"."); + } + + [TestMethod] + public void GetRegistrationForResourceTypeName_returns_correct_value_for_registered_names() + { + // Arrange + var mockPostRegistration = new Mock(MockBehavior.Strict); + mockPostRegistration.Setup(m => m.Type).Returns(typeof(Post)); + mockPostRegistration.Setup(m => m.ResourceTypeName).Returns("posts"); + + var mockAuthorRegistration = new Mock(MockBehavior.Strict); + mockAuthorRegistration.Setup(m => m.Type).Returns(typeof(Author)); + mockAuthorRegistration.Setup(m => m.ResourceTypeName).Returns("authors"); + + var registry = new ResourceTypeRegistry(); + registry.AddRegistration(mockPostRegistration.Object); + registry.AddRegistration(mockAuthorRegistration.Object); + + // Act + var postReg = registry.GetRegistrationForResourceTypeName("posts"); + var authorReg = registry.GetRegistrationForResourceTypeName("authors"); + + // Assert + postReg.Should().BeSameAs(mockPostRegistration.Object); + authorReg.Should().BeSameAs(mockAuthorRegistration.Object); + } + + [TestMethod] + public void TypeIsRegistered_returns_true_if_type_is_registered() + { + // Arrange + var mockPostRegistration = new Mock(MockBehavior.Strict); + mockPostRegistration.Setup(m => m.Type).Returns(typeof(Post)); + mockPostRegistration.Setup(m => m.ResourceTypeName).Returns("posts"); + + var registry = new ResourceTypeRegistry(); + registry.AddRegistration(mockPostRegistration.Object); + + // Act + var isRegistered = registry.TypeIsRegistered(typeof(Post)); + + // Assert + isRegistered.Should().BeTrue(); + } + + [TestMethod] + public void TypeIsRegistered_returns_true_if_parent_type_is_registered() + { + // Arrange + var mockPostRegistration = new Mock(MockBehavior.Strict); + mockPostRegistration.Setup(m => m.Type).Returns(typeof(Post)); + mockPostRegistration.Setup(m => m.ResourceTypeName).Returns("posts"); + + var registry = new ResourceTypeRegistry(); + registry.AddRegistration(mockPostRegistration.Object); + + // Act + var isRegistered = registry.TypeIsRegistered(typeof(DerivedPost)); + + // Assert + isRegistered.Should().BeTrue(); + } + + [TestMethod] + public void TypeIsRegistered_returns_false_if_no_type_in_hierarchy_is_registered() + { + // Arrange + var registry = new ResourceTypeRegistry(); + + // Act + var isRegistered = registry.TypeIsRegistered(typeof(Comment)); + + // Assert + isRegistered.Should().BeFalse(); + } + } +} diff --git a/JSONAPI.Tests/Data/AttributeSerializationTest.json b/JSONAPI.Tests/Data/AttributeSerializationTest.json deleted file mode 100644 index 1a4fa932..00000000 --- a/JSONAPI.Tests/Data/AttributeSerializationTest.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "samples": [ - { - "id": "1", - "booleanField": false, - "nullableBooleanField": false, - "sByteField": 0, - "nullableSByteField": null, - "byteField": 0, - "nullableByteField": null, - "int16Field": 0, - "nullableInt16Field": null, - "uInt16Field": 0, - "nullableUInt16Field": null, - "int32Field": 0, - "nullableInt32Field": null, - "uInt32Field": 0, - "nullableUInt32Field": null, - "int64Field": 0, - "nullableInt64Field": null, - "uInt64Field": 0, - "nullableUInt64Field": null, - "doubleField": 0.0, - "nullableDoubleField": null, - "singleField": 0.0, - "nullableSingleField": null, - "decimalField": "0", - "nullableDecimalField": null, - "dateTimeField": "0001-01-01T00:00:00", - "nullableDateTimeField": null, - "dateTimeOffsetField": "0001-01-01T00:00:00+00:00", - "nullableDateTimeOffsetField": null, - "guidField": "00000000-0000-0000-0000-000000000000", - "nullableGuidField": null, - "stringField": null, - "enumField": 0, - "nullableEnumField": null - }, { - "id": "2", - "booleanField": true, - "nullableBooleanField": true, - "sByteField": 123, - "nullableSByteField": 123, - "byteField": 253, - "nullableByteField": 253, - "int16Field": 32000, - "nullableInt16Field": 32000, - "uInt16Field": 64000, - "nullableUInt16Field": 64000, - "int32Field": 2000000000, - "nullableInt32Field": 2000000000, - "uInt32Field": 3000000000, - "nullableUInt32Field": 3000000000, - "int64Field": 9223372036854775807, - "nullableInt64Field": 9223372036854775807, - "uInt64Field": 9223372036854775808, - "nullableUInt64Field": 9223372036854775808, - "doubleField": 1056789.123, - "nullableDoubleField": 1056789.123, - "singleField": 1056789.13, - "nullableSingleField": 1056789.13, - "decimalField": "1056789.123", - "nullableDecimalField": "1056789.123", - "dateTimeField": "1776-07-04T00:00:00", - "nullableDateTimeField": "1776-07-04T00:00:00", - "dateTimeOffsetField": "1776-07-04T00:00:00-05:00", - "nullableDateTimeOffsetField": "1776-07-04T00:00:00-05:00", - "guidField": "6566f9b4-5245-40de-890d-98b40a4ad656", - "nullableGuidField": "3d1fb81e-43ee-4d04-af91-c8a326341293", - "stringField": "Some string 156", - "enumField": 1, - "nullableEnumField": 2 - } - ] -} diff --git a/JSONAPI.Tests/Data/ByteIdSerializationTest.json b/JSONAPI.Tests/Data/ByteIdSerializationTest.json deleted file mode 100644 index f4063a5b..00000000 --- a/JSONAPI.Tests/Data/ByteIdSerializationTest.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "tags": [ - { - "id": "1", - "text": "Ember" - }, { - "id": "2", - "text": "React" - }, { - "id": "3", - "text": "Angular" - } - ] -} diff --git a/JSONAPI.Tests/Data/DeserializeRawJsonTest.json b/JSONAPI.Tests/Data/DeserializeRawJsonTest.json deleted file mode 100644 index 684bb5ff..00000000 --- a/JSONAPI.Tests/Data/DeserializeRawJsonTest.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "comments": [ - { - "id": "2", - "customData": null - }, - { - "id": "4", - "customData": { - "foo": "bar" - } - } - ] -} diff --git a/JSONAPI.Tests/Data/ErrorSerializerTest.json b/JSONAPI.Tests/Data/ErrorSerializerTest.json deleted file mode 100644 index 5d7446eb..00000000 --- a/JSONAPI.Tests/Data/ErrorSerializerTest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "errors": [ - { - "id": "OUTER-ID", - "status": "500", - "title": "System.Exception", - "detail": "Outer exception message", - "stackTrace": "Outer stack trace", - "inner": { - "id": "INNER-ID", - "status": "500", - "title": "Castle.Proxies.ExceptionProxy", - "detail": "Inner exception message", - "stackTrace": "Inner stack trace", - "inner": null - } - } - ] -} \ No newline at end of file diff --git a/JSONAPI.Tests/Data/FormatterErrorSerializationTest.json b/JSONAPI.Tests/Data/FormatterErrorSerializationTest.json deleted file mode 100644 index f5439ca2..00000000 --- a/JSONAPI.Tests/Data/FormatterErrorSerializationTest.json +++ /dev/null @@ -1 +0,0 @@ -{"test":"foo"} \ No newline at end of file diff --git a/JSONAPI.Tests/Data/LinkTemplateTest.json b/JSONAPI.Tests/Data/LinkTemplateTest.json deleted file mode 100644 index 6d351b09..00000000 --- a/JSONAPI.Tests/Data/LinkTemplateTest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "posts": { - "id": "2", - "title": "How to fry an egg", - "links": { - "author": { - "href": "/users/5" - } - } - } -} \ No newline at end of file diff --git a/JSONAPI.Tests/Data/MalformedRawJsonString.json b/JSONAPI.Tests/Data/MalformedRawJsonString.json deleted file mode 100644 index 54c335f8..00000000 --- a/JSONAPI.Tests/Data/MalformedRawJsonString.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "comments": [ - { - "id": "5", - "body": null, - "customData": { }, - "links": { - "post": null - } - } - ] -} diff --git a/JSONAPI.Tests/Data/NonStandardIdTest.json b/JSONAPI.Tests/Data/NonStandardIdTest.json deleted file mode 100644 index 9acfc295..00000000 --- a/JSONAPI.Tests/Data/NonStandardIdTest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "nonStandardIdThings": [ - { - "id": "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f", - "uuid": "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f", - "data": "Swap" - } - ] -} \ No newline at end of file diff --git a/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json b/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json deleted file mode 100644 index 49894bcf..00000000 --- a/JSONAPI.Tests/Data/ReformatsRawJsonStringWithUnquotedKeys.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "comments": [ - { - "id": "5", - "body": null, - "customData": { - "unquotedKey": 5 - }, - "links": { - "post": null - } - } - ] -} diff --git a/JSONAPI.Tests/Data/SerializerIntegrationTest.json b/JSONAPI.Tests/Data/SerializerIntegrationTest.json deleted file mode 100644 index 3daee7ff..00000000 --- a/JSONAPI.Tests/Data/SerializerIntegrationTest.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "posts": [ - { - "id": "1", - "title": "Linkbait!", - "links": { - "comments": [ "2", "3", "4" ], - "author": "1" - } - }, - { - "id": "2", - "title": "Rant #1023", - "links": { - "comments": [ "5" ], - "author": "1" - } - }, - { - "id": "3", - "title": "Polemic in E-flat minor #824", - "links": { - "comments": [ ], - "author": "1" - } - }, - { - "id": "4", - "title": "This post has no author.", - "links": { - "comments": [ ], - "author": null - } - } - ], - "linked": { - "comments": [ - { - "id": "2", - "body": "Nuh uh!", - "customData": null, - "links": { "post": "1" } - }, - { - "id": "3", - "body": "Yeah huh!", - "customData": null, - "links": { - "post": "1" - } - }, - { - "id": "4", - "body": "Third Reich.", - "customData": { - "foo": "bar" - }, - "links": { - "post": "1" - } - }, - { - "id": "5", - "body": "I laughed, I cried!", - "customData": null, - "links": { - "post": "2" - } - } - ], - "authors": [ - { - "id": "1", - "name": "Jason Hater", - "links": { - "posts": [ "1", "2", "3" ] - } - } - ] - } -} diff --git a/JSONAPI.Tests/Documents/Builders/ErrorDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/ErrorDocumentBuilderTests.cs new file mode 100644 index 00000000..e5fef359 --- /dev/null +++ b/JSONAPI.Tests/Documents/Builders/ErrorDocumentBuilderTests.cs @@ -0,0 +1,162 @@ +using System; +using System.Linq; +using System.Net; +using FluentAssertions; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Tests.Documents.Builders +{ + [TestClass] + public class ErrorDocumentBuilderTests + { + private const string GuidRegex = @"\b[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}\b"; + + [TestMethod] + public void Builds_document_from_exception() + { + // Arrange + Exception theException; + try + { + throw new Exception("This is the exception!"); + } + catch (Exception ex) + { + theException = ex; + } + + // Act + var errorDocumentBuilder = new ErrorDocumentBuilder(); + var document = errorDocumentBuilder.BuildFromException(theException); + + // Assert + document.Errors.Length.Should().Be(1); + var error = document.Errors.First(); + error.Id.Should().MatchRegex(GuidRegex); + error.Title.Should().Be("Unhandled exception"); + error.Detail.Should().Be("An unhandled exception was thrown while processing the request."); + error.Status.Should().Be(HttpStatusCode.InternalServerError); + ((string)error.Metadata.MetaObject["exceptionMessage"]).Should().Be("This is the exception!"); + ((string)error.Metadata.MetaObject["stackTrace"]).Should().NotBeNull(); + } + + [TestMethod] + public void Builds_document_from_exception_with_inner_exception() + { + // Arrange + Exception theException; + try + { + try + { + throw new Exception("This is the inner exception!"); + } + catch (Exception ex) + { + throw new Exception("This is the outer exception!", ex); + } + } + catch (Exception ex) + { + theException = ex; + } + + // Act + var errorDocumentBuilder = new ErrorDocumentBuilder(); + var document = errorDocumentBuilder.BuildFromException(theException); + + // Assert + document.Errors.Length.Should().Be(1); + var error = document.Errors.First(); + error.Id.Should().MatchRegex(GuidRegex); + error.Title.Should().Be("Unhandled exception"); + error.Detail.Should().Be("An unhandled exception was thrown while processing the request."); + error.Status.Should().Be(HttpStatusCode.InternalServerError); + ((string)error.Metadata.MetaObject["exceptionMessage"]).Should().Be("This is the outer exception!"); + ((string)error.Metadata.MetaObject["stackTrace"]).Should().NotBeNull(); + + var inner = (JObject)error.Metadata.MetaObject["innerException"]; + ((string)inner["exceptionMessage"]).Should().Be("This is the inner exception!"); + ((string)inner["stackTrace"]).Should().NotBeNull(); + } + + [TestMethod] + public void Builds_document_from_exception_with_two_levels_deep_inner_exception() + { + // Arrange + Exception theException; + try + { + try + { + try + { + throw new Exception("This is the inner exception!"); + } + catch (Exception ex) + { + throw new Exception("This is the middle exception!", ex); + } + } + catch (Exception ex) + { + throw new Exception("This is the outer exception!", ex); + } + } + catch (Exception ex) + { + theException = ex; + } + + // Act + var errorDocumentBuilder = new ErrorDocumentBuilder(); + var document = errorDocumentBuilder.BuildFromException(theException); + + // Assert + document.Errors.Length.Should().Be(1); + var error = document.Errors.First(); + error.Id.Should().MatchRegex(GuidRegex); + error.Title.Should().Be("Unhandled exception"); + error.Detail.Should().Be("An unhandled exception was thrown while processing the request."); + error.Status.Should().Be(HttpStatusCode.InternalServerError); + ((string)error.Metadata.MetaObject["exceptionMessage"]).Should().Be("This is the outer exception!"); + ((string)error.Metadata.MetaObject["stackTrace"]).Should().NotBeNull(); + + var middle = (JObject)error.Metadata.MetaObject["innerException"]; + ((string)middle["exceptionMessage"]).Should().Be("This is the middle exception!"); + ((string)middle["stackTrace"]).Should().NotBeNull(); + + var inner = (JObject)middle["innerException"]; + ((string)inner["exceptionMessage"]).Should().Be("This is the inner exception!"); + ((string)inner["stackTrace"]).Should().NotBeNull(); + } + + [TestMethod] + public void Builds_document_from_JsonApiException() + { + // Arrange + var mockError = new Mock(MockBehavior.Strict); + JsonApiException theException; + try + { + throw new JsonApiException(mockError.Object); + } + catch (JsonApiException ex) + { + theException = ex; + } + + // Act + var errorDocumentBuilder = new ErrorDocumentBuilder(); + var document = errorDocumentBuilder.BuildFromException(theException); + + // Assert + document.Errors.Length.Should().Be(1); + document.Errors.First().Should().Be(mockError.Object); + } + } +} diff --git a/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs new file mode 100644 index 00000000..a6b9f14d --- /dev/null +++ b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs @@ -0,0 +1,151 @@ +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; +using JSONAPI.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace JSONAPI.Tests.Documents.Builders +{ + [TestClass] + public class FallbackDocumentBuilderTests + { + private const string GuidRegex = @"\b[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}\b"; + + class Fruit + { + public string Id { get; set; } + + public string Name { get; set; } + } + + [TestMethod] + public async Task Creates_single_resource_document_for_registered_non_collection_types() + { + // Arrange + var objectContent = new Fruit { Id = "984", Name = "Kiwi" }; + + var mockDocument = new Mock(MockBehavior.Strict); + var includePathExpression = new string[] {}; + + var singleResourceDocumentBuilder = new Mock(MockBehavior.Strict); + singleResourceDocumentBuilder.Setup(b => b.BuildDocument(objectContent, It.IsAny(), includePathExpression, null, null)).Returns(mockDocument.Object); + + var mockQueryableDocumentBuilder = new Mock(MockBehavior.Strict); + var mockResourceCollectionDocumentBuilder = new Mock(MockBehavior.Strict); + + var cancellationTokenSource = new CancellationTokenSource(); + + var request = new HttpRequestMessage(HttpMethod.Get, "https://www.example.com/fruits"); + var mockBaseUrlService = new Mock(MockBehavior.Strict); + mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com"); + + var mockSortExpressionExtractor = new Mock(MockBehavior.Strict); + mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(new[] { "id " }); + + var mockIncludeExpressionExtractor = new Mock(MockBehavior.Strict); + mockIncludeExpressionExtractor.Setup(e => e.ExtractIncludeExpressions(request)).Returns(includePathExpression); + + // Act + var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object, + mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockIncludeExpressionExtractor.Object, mockBaseUrlService.Object); + var resultDocument = await fallbackDocumentBuilder.BuildDocument(objectContent, request, cancellationTokenSource.Token); + + // Assert + resultDocument.Should().BeSameAs(mockDocument.Object); + } + + [TestMethod] + public async Task Creates_resource_collection_document_for_queryables() + { + // Arrange + var items = new[] + { + new Fruit {Id = "43", Name = "Strawberry"}, + new Fruit {Id = "43", Name = "Grape"} + }.AsQueryable(); + + var mockDocument = new Mock(MockBehavior.Strict); + + var singleResourceDocumentBuilder = new Mock(MockBehavior.Strict); + + var request = new HttpRequestMessage(); + + var mockBaseUrlService = new Mock(MockBehavior.Strict); + mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com/"); + + var sortExpressions = new[] { "id" }; + + var includeExpressions = new string[] { }; + + var cancellationTokenSource = new CancellationTokenSource(); + + var mockQueryableDocumentBuilder = new Mock(MockBehavior.Strict); + mockQueryableDocumentBuilder + .Setup(b => b.BuildDocument(items, request, sortExpressions, cancellationTokenSource.Token, includeExpressions)) + .Returns(Task.FromResult(mockDocument.Object)); + + var mockResourceCollectionDocumentBuilder = new Mock(MockBehavior.Strict); + + var mockSortExpressionExtractor = new Mock(MockBehavior.Strict); + mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(sortExpressions); + + var mockIncludeExpressionExtractor = new Mock(MockBehavior.Strict); + mockIncludeExpressionExtractor.Setup(e => e.ExtractIncludeExpressions(request)).Returns(includeExpressions); + + // Act + var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object, + mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockIncludeExpressionExtractor.Object, mockBaseUrlService.Object); + var resultDocument = await fallbackDocumentBuilder.BuildDocument(items, request, cancellationTokenSource.Token); + + // Assert + resultDocument.Should().BeSameAs(mockDocument.Object); + } + + [TestMethod] + public async Task Creates_resource_collection_document_for_non_queryable_enumerables() + { + // Arrange + var items = new[] + { + new Fruit {Id = "43", Name = "Strawberry"}, + new Fruit {Id = "43", Name = "Grape"} + }; + + var mockDocument = new Mock(MockBehavior.Strict); + + var singleResourceDocumentBuilder = new Mock(MockBehavior.Strict); + + var cancellationTokenSource = new CancellationTokenSource(); + + var request = new HttpRequestMessage(HttpMethod.Get, "https://www.example.com/fruits"); + + var mockBaseUrlService = new Mock(MockBehavior.Strict); + mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com/"); + + var mockQueryableDocumentBuilder = new Mock(MockBehavior.Strict); + var mockResourceCollectionDocumentBuilder = new Mock(MockBehavior.Strict); + mockResourceCollectionDocumentBuilder + .Setup(b => b.BuildDocument(items, "https://www.example.com/", It.IsAny(), It.IsAny(), null)) + .Returns(() => (mockDocument.Object)); + + var mockSortExpressionExtractor = new Mock(MockBehavior.Strict); + mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(new[] { "id " }); + + var mockIncludeExpressionExtractor = new Mock(MockBehavior.Strict); + mockIncludeExpressionExtractor.Setup(e => e.ExtractIncludeExpressions(request)).Returns(new string[] { }); + + // Act + var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object, + mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockIncludeExpressionExtractor.Object, mockBaseUrlService.Object); + var resultDocument = await fallbackDocumentBuilder.BuildDocument(items, request, cancellationTokenSource.Token); + + // Assert + resultDocument.Should().BeSameAs(mockDocument.Object); + } + } +} diff --git a/JSONAPI.Tests/Documents/Builders/RegistryDrivenDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/RegistryDrivenDocumentBuilderTests.cs new file mode 100644 index 00000000..ff943102 --- /dev/null +++ b/JSONAPI.Tests/Documents/Builders/RegistryDrivenDocumentBuilderTests.cs @@ -0,0 +1,122 @@ +using FluentAssertions; +using JSONAPI.Documents.Builders; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Tests.Documents.Builders +{ + [TestClass] + public class RegistryDrivenDocumentBuilderTests + { + [TestMethod] + public void PathExpressionMatchesCurrentPath_is_true_when_pathToInclude_equals_currentPath_with_one_segment() + { + // Arrange + const string currentPath = "posts"; + const string pathToInclude = "posts"; + + // Act + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + + // Assert + matches.Should().BeTrue(); + } + + [TestMethod] + public void PathExpressionMatchesCurrentPath_is_false_when_pathToInclude_does_not_equal_or_start_with_currentPath() + { + // Arrange + const string currentPath = "posts"; + const string pathToInclude = "comments"; + + // Act + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + + // Assert + matches.Should().BeFalse(); + } + + [TestMethod] + public void PathExpressionMatchesCurrentPath_is_false_when_pathToInclude_is_empty() + { + // Arrange + const string currentPath = ""; + const string pathToInclude = ""; + + // Act + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + + // Assert + matches.Should().BeFalse(); + } + + [TestMethod] + public void PathExpressionMatchesCurrentPath_is_false_when_pathToInclude_is_null() + { + // Arrange + const string currentPath = null; + const string pathToInclude = null; + + // Act + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + + // Assert + matches.Should().BeFalse(); + } + + [TestMethod] + public void PathExpressionMatchesCurrentPath_is_true_when_pathToInclude_equals_currentPath_with_multiple_segments() + { + // Arrange + const string currentPath = "posts.author"; + const string pathToInclude = "posts.author"; + + // Act + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + + // Assert + matches.Should().BeTrue(); + } + + [TestMethod] + public void PathExpressionMatchesCurrentPath_is_true_when_all_segments_of_currentPath_are_contained_by_pathToInclude() + { + // Arrange + const string currentPath = "posts.author"; + const string pathToInclude = "posts.author.comments"; + + // Act + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + + // Assert + matches.Should().BeTrue(); + } + + [TestMethod] + public void PathExpressionMatchesCurrentPath_is_false_when_all_segments_of_currentPath_are_contained_by_pathToInclude_but_start_doesnt_match() + { + // Arrange + const string currentPath = "posts.author"; + const string pathToInclude = "author.posts.author"; + + // Act + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + + // Assert + matches.Should().BeFalse(); + } + + [TestMethod] + public void PathExpressionMatchesCurrentPath_is_false_when_pathToInclude_starts_with_currentPath_but_segments_differ() + { + // Arrange + const string currentPath = "posts.author"; + const string pathToInclude = "posts.authora"; + + // Act + var matches = RegistryDrivenDocumentBuilder.PathExpressionMatchesCurrentPath(currentPath, pathToInclude); + + // Assert + matches.Should().BeFalse(); + } + } +} diff --git a/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs new file mode 100644 index 00000000..f5afb353 --- /dev/null +++ b/JSONAPI.Tests/Documents/Builders/RegistryDrivenSingleResourceDocumentBuilderTests.cs @@ -0,0 +1,288 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using JSONAPI.Core; +using JSONAPI.Documents; +using JSONAPI.Documents.Builders; +using JSONAPI.Json; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Newtonsoft.Json.Linq; + +namespace JSONAPI.Tests.Documents.Builders +{ + [TestClass] + public class RegistryDrivenSingleResourceDocumentBuilderTests + { + class Country + { + public string Id { get; set; } + + public string Name { get; set; } + + public Continent Continent { get; set; } + + public ICollection Provinces { get; set; } + + public ICollection Cities { get; set; } + } + + class Province + { + public string Id { get; set; } + + public string Name { get; set; } + + public City Capital { get; set; } + } + + class City + { + public string Id { get; set; } + + public string Name { get; set; } + } + + class Continent + { + public string Id { get; set; } + + public string Name { get; set; } + + public ICollection Countries { get; set; } + } + + [TestMethod] + public void Returns_correct_document_for_resource() + { + // Arrange + var city1 = new City + { + Id = "10", + Name = "Madrid" + }; + + var city2 = new City + { + Id = "11", + Name = "Barcelona" + }; + + var city3 = new City + { + Id = "12", + Name = "Badajoz" + }; + + var province1 = new Province + { + Id = "506", + Name = "Badajoz", + Capital = city3 + }; + + var province2 = new Province + { + Id = "507", + Name = "Cuenca", + Capital = null // Leaving null to test a null to-one + }; + + var continent = new Continent + { + Id = "1", + Name = "Europe" + }; + + var country = new Country + { + Id = "4", + Name = "Spain", + Continent = continent, + Provinces = new List { province1, province2 }, + Cities = new List { city1, city2, city3 } + }; + + + // Country registration + var countryName = + new ResourceTypeAttribute( + new PrimitiveTypeAttributeValueConverter(typeof(Country).GetProperty("Name")), null, "name"); + var countryCities = + new ToManyResourceTypeRelationship(typeof(Country).GetProperty("Cities"), "cities", typeof(City), null, null); + var countryProvinces = + new ToManyResourceTypeRelationship(typeof(Country).GetProperty("Provinces"), "provinces", typeof(Province), null, null); + var countryContinent = + new ToOneResourceTypeRelationship(typeof(Country).GetProperty("Continent"), "continent", typeof(Continent), null, null); + var countryRegistration = new Mock(MockBehavior.Strict); + countryRegistration.Setup(m => m.GetIdForResource(It.IsAny())).Returns((Country c) => country.Id); + countryRegistration.Setup(m => m.ResourceTypeName).Returns("countries"); + countryRegistration.Setup(m => m.Attributes).Returns(new[] { countryName }); + countryRegistration + .Setup(m => m.Relationships) + .Returns(() => new ResourceTypeRelationship[] { countryCities, countryProvinces, countryContinent }); + + + // City registration + var cityName = + new ResourceTypeAttribute( + new PrimitiveTypeAttributeValueConverter(typeof(City).GetProperty("Name")), null, "name"); + var cityRegistration = new Mock(MockBehavior.Strict); + cityRegistration.Setup(m => m.ResourceTypeName).Returns("cities"); + cityRegistration.Setup(m => m.GetIdForResource(It.IsAny())).Returns((City c) => c.Id); + cityRegistration.Setup(m => m.Attributes).Returns(new[] { cityName }); + cityRegistration.Setup(m => m.Relationships).Returns(new ResourceTypeRelationship[] { }); + + + // Province registration + var provinceName = + new ResourceTypeAttribute( + new PrimitiveTypeAttributeValueConverter(typeof(Province).GetProperty("Name")), null, "name"); + var provinceCapital = new ToOneResourceTypeRelationship(typeof(Province).GetProperty("Capital"), "capital", typeof(City), null, null); + var provinceRegistration = new Mock(MockBehavior.Strict); + provinceRegistration.Setup(m => m.ResourceTypeName).Returns("provinces"); + provinceRegistration.Setup(m => m.GetIdForResource(It.IsAny())).Returns((Province c) => c.Id); + provinceRegistration.Setup(m => m.Attributes).Returns(new[] { provinceName }); + provinceRegistration + .Setup(m => m.Relationships) + .Returns(() => new ResourceTypeRelationship[] { provinceCapital }); + + + // Continent registration + var continentName = + new ResourceTypeAttribute( + new PrimitiveTypeAttributeValueConverter(typeof(Continent).GetProperty("Name")), null, "name"); + var continentCountries = + new ToManyResourceTypeRelationship(typeof(Continent).GetProperty("Countries"), "countries", typeof(Country), null, null); + var continentRegistration = new Mock(MockBehavior.Strict); + continentRegistration.Setup(m => m.ResourceTypeName).Returns("continents"); + continentRegistration.Setup(m => m.GetIdForResource(It.IsAny())).Returns((Continent c) => c.Id); + continentRegistration.Setup(m => m.Attributes).Returns(new[] { continentName }); + continentRegistration + .Setup(m => m.Relationships) + .Returns(() => new ResourceTypeRelationship[] { continentCountries }); + + var mockRegistry = new Mock(MockBehavior.Strict); + mockRegistry.Setup(r => r.GetRegistrationForType(typeof(Country))).Returns(countryRegistration.Object); + mockRegistry.Setup(r => r.GetRegistrationForType(typeof(City))).Returns(cityRegistration.Object); + mockRegistry.Setup(r => r.GetRegistrationForType(typeof(Province))).Returns(provinceRegistration.Object); + mockRegistry.Setup(r => r.GetRegistrationForType(typeof(Continent))).Returns(continentRegistration.Object); + + var linkConventions = new DefaultLinkConventions(); + + var metadataObject = new JObject(); + metadataObject["baz"] = "qux"; + var metadata = new BasicMetadata(metadataObject); + + // Act + var documentBuilder = new RegistryDrivenSingleResourceDocumentBuilder(mockRegistry.Object, linkConventions); + var document = documentBuilder.BuildDocument(country, "http://www.example.com", new[] { "provinces.capital", "continent" }, metadata, null); + + // Assert + document.PrimaryData.Id.Should().Be("4"); + document.PrimaryData.Type.Should().Be("countries"); + ((string) document.PrimaryData.Attributes["name"]).Should().Be("Spain"); + document.PrimaryData.Relationships.Count.Should().Be(3); + + var citiesRelationship = document.PrimaryData.Relationships.First(); + citiesRelationship.Key.Should().Be("cities"); + citiesRelationship.Value.SelfLink.Href.Should().Be("http://www.example.com/countries/4/relationships/cities"); + citiesRelationship.Value.RelatedResourceLink.Href.Should().Be("http://www.example.com/countries/4/cities"); + citiesRelationship.Value.Linkage.Should().BeNull(); + + var provincesRelationship = document.PrimaryData.Relationships.Skip(1).First(); + provincesRelationship.Key.Should().Be("provinces"); + provincesRelationship.Value.SelfLink.Href.Should().Be("http://www.example.com/countries/4/relationships/provinces"); + provincesRelationship.Value.RelatedResourceLink.Href.Should().Be("http://www.example.com/countries/4/provinces"); + provincesRelationship.Value.Linkage.IsToMany.Should().BeTrue(); + provincesRelationship.Value.Linkage.Identifiers[0].Type.Should().Be("provinces"); + provincesRelationship.Value.Linkage.Identifiers[0].Id.Should().Be("506"); + provincesRelationship.Value.Linkage.Identifiers[1].Type.Should().Be("provinces"); + provincesRelationship.Value.Linkage.Identifiers[1].Id.Should().Be("507"); + + var continentRelationship = document.PrimaryData.Relationships.Skip(2).First(); + AssertToOneRelationship(continentRelationship, "continent", + "http://www.example.com/countries/4/relationships/continent", + "http://www.example.com/countries/4/continent", + "continents", "1"); + + document.RelatedData.Length.Should().Be(4); // 2 provinces, 1 city, and 1 continent + + var province1RelatedData = document.RelatedData[0]; + province1RelatedData.Id.Should().Be("506"); + province1RelatedData.Attributes["name"].Value().Should().Be("Badajoz"); + province1RelatedData.Type.Should().Be("provinces"); + province1RelatedData.Relationships.Count.Should().Be(1); + + var province1CapitalRelationship = province1RelatedData.Relationships.First(); + AssertToOneRelationship(province1CapitalRelationship, "capital", + "http://www.example.com/provinces/506/relationships/capital", + "http://www.example.com/provinces/506/capital", + "cities", "12"); + + var province2RelatedData = document.RelatedData[1]; + province2RelatedData.Id.Should().Be("507"); + province2RelatedData.Type.Should().Be("provinces"); + province2RelatedData.Attributes["name"].Value().Should().Be("Cuenca"); + + var province2CapitalRelationship = province2RelatedData.Relationships.First(); + AssertEmptyToOneRelationship(province2CapitalRelationship, "capital", + "http://www.example.com/provinces/507/relationships/capital", + "http://www.example.com/provinces/507/capital"); + + var city3RelatedData = document.RelatedData[2]; + city3RelatedData.Id.Should().Be("12"); + city3RelatedData.Type.Should().Be("cities"); + city3RelatedData.Attributes["name"].Value().Should().Be("Badajoz"); + + var continentRelatedData = document.RelatedData[3]; + continentRelatedData.Id.Should().Be("1"); + continentRelatedData.Type.Should().Be("continents"); + continentRelatedData.Attributes["name"].Value().Should().Be("Europe"); + continentRelatedData.Relationships.Count.Should().Be(1); + var continentCountriesRelationship = continentRelatedData.Relationships.First(); + continentCountriesRelationship.Key.Should().Be("countries"); + continentCountriesRelationship.Value.SelfLink.Href.Should().Be("http://www.example.com/continents/1/relationships/countries"); + continentCountriesRelationship.Value.RelatedResourceLink.Href.Should().Be("http://www.example.com/continents/1/countries"); + continentCountriesRelationship.Value.Linkage.Should().BeNull(); + + ((string) document.Metadata.MetaObject["baz"]).Should().Be("qux"); + } + + [TestMethod] + public void Returns_correct_document_for_null_resource() + { + // Arrange + var mockRegistry = new Mock(MockBehavior.Strict); + var linkConventions = new DefaultLinkConventions(); + + // Act + var documentBuilder = new RegistryDrivenSingleResourceDocumentBuilder(mockRegistry.Object, linkConventions); + var document = documentBuilder.BuildDocument(null, "http://www.example.com", null, null, null); + + // Assert + document.PrimaryData.Should().BeNull(); + } + + private void AssertToOneRelationship(KeyValuePair relationshipPair, string keyName, string selfLink, string relatedResourceLink, + string linkageType, string linkageId) + { + relationshipPair.Key.Should().Be(keyName); + relationshipPair.Value.SelfLink.Href.Should().Be(selfLink); + relationshipPair.Value.RelatedResourceLink.Href.Should().Be(relatedResourceLink); + relationshipPair.Value.Linkage.IsToMany.Should().BeFalse(); + relationshipPair.Value.Linkage.Identifiers.Length.Should().Be(1); + relationshipPair.Value.Linkage.Identifiers[0].Type.Should().Be(linkageType); + relationshipPair.Value.Linkage.Identifiers[0].Id.Should().Be(linkageId); + } + + private void AssertEmptyToOneRelationship(KeyValuePair relationshipPair, string keyName, string selfLink, string relatedResourceLink) + { + relationshipPair.Key.Should().Be(keyName); + relationshipPair.Value.SelfLink.Href.Should().Be(selfLink); + relationshipPair.Value.RelatedResourceLink.Href.Should().Be(relatedResourceLink); + relationshipPair.Value.Linkage.IsToMany.Should().BeFalse(); + relationshipPair.Value.Linkage.Identifiers.Length.Should().Be(0); + } + } +} diff --git a/JSONAPI.Tests/Documents/DefaultLinkConventionsTests.cs b/JSONAPI.Tests/Documents/DefaultLinkConventionsTests.cs new file mode 100644 index 00000000..f34c93bd --- /dev/null +++ b/JSONAPI.Tests/Documents/DefaultLinkConventionsTests.cs @@ -0,0 +1,190 @@ +using System.Collections.Generic; +using FluentAssertions; +using JSONAPI.Core; +using JSONAPI.Documents; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace JSONAPI.Tests.Documents +{ + [TestClass] + public class DefaultLinkConventionsTests + { + class Country + { + public string Id { get; set; } + + public ICollection Cities { get; set; } + } + + class City + { + public string Id { get; set; } + } + + [TestMethod] + public void GetRelationshipLink_returns_default_url_for_relationship() + { + // Arrange + var relationshipOwner = new Country { Id = "45" }; + var relationshipProperty = new ToManyResourceTypeRelationship(typeof (Country).GetProperty("Cities"), + "cities", typeof (City), null, null); + var mockTypeRegistration = new Mock(MockBehavior.Strict); + mockTypeRegistration.Setup(r => r.ResourceTypeName).Returns("countries"); + mockTypeRegistration.Setup(r => r.GetIdForResource(relationshipOwner)).Returns("45"); + var mockRegistry = new Mock(MockBehavior.Strict); + mockRegistry.Setup(m => m.GetRegistrationForType(typeof(Country))).Returns(mockTypeRegistration.Object); + + // Act + var conventions = new DefaultLinkConventions(); + var relationshipLink = conventions.GetRelationshipLink(relationshipOwner, mockRegistry.Object, relationshipProperty, "https://www.example.com"); + + // Assert + relationshipLink.Href.Should().Be("https://www.example.com/countries/45/relationships/cities"); + } + + [TestMethod] + public void GetRelationshipLink_returns_default_url_for_relationship_when_base_url_has_trailing_slash() + { + // Arrange + var relationshipOwner = new Country { Id = "45" }; + var relationshipProperty = new ToManyResourceTypeRelationship(typeof(Country).GetProperty("Cities"), + "cities", typeof(City), null, null); + var mockTypeRegistration = new Mock(MockBehavior.Strict); + mockTypeRegistration.Setup(r => r.ResourceTypeName).Returns("countries"); + mockTypeRegistration.Setup(r => r.GetIdForResource(relationshipOwner)).Returns("45"); + var mockRegistry = new Mock(MockBehavior.Strict); + mockRegistry.Setup(m => m.GetRegistrationForType(typeof(Country))).Returns(mockTypeRegistration.Object); + + // Act + var conventions = new DefaultLinkConventions(); + var relationshipLink = conventions.GetRelationshipLink(relationshipOwner, mockRegistry.Object, relationshipProperty, "https://www.example.com"); + + // Assert + relationshipLink.Href.Should().Be("https://www.example.com/countries/45/relationships/cities"); + } + + [TestMethod] + public void GetRelationshipLink_is_correct_if_template_is_present() + { + // Arrange + var relationshipOwner = new Country { Id = "45" }; + var relationshipProperty = new ToManyResourceTypeRelationship(typeof(Country).GetProperty("Cities"), + "cities", typeof(City), "foo/{1}/bar", null); + var mockTypeRegistration = new Mock(MockBehavior.Strict); + mockTypeRegistration.Setup(r => r.GetIdForResource(relationshipOwner)).Returns("45"); + var mockRegistry = new Mock(MockBehavior.Strict); + mockRegistry.Setup(m => m.GetRegistrationForType(typeof(Country))).Returns(mockTypeRegistration.Object); + + // Act + var conventions = new DefaultLinkConventions(); + var relationshipLink = conventions.GetRelationshipLink(relationshipOwner, mockRegistry.Object, relationshipProperty, "https://www.example.com"); + + // Assert + relationshipLink.Href.Should().Be("https://www.example.com/foo/45/bar"); + } + + [TestMethod] + public void GetRelationshipLink_is_correct_if_template_is_present_and_base_url_has_trailing_slash() + { + // Arrange + var relationshipOwner = new Country { Id = "45" }; + var relationshipProperty = new ToManyResourceTypeRelationship(typeof(Country).GetProperty("Cities"), + "cities", typeof(City), "foo/{1}/bar", null); + var mockTypeRegistration = new Mock(MockBehavior.Strict); + mockTypeRegistration.Setup(r => r.GetIdForResource(relationshipOwner)).Returns("45"); + var mockRegistry = new Mock(MockBehavior.Strict); + mockRegistry.Setup(m => m.GetRegistrationForType(typeof(Country))).Returns(mockTypeRegistration.Object); + + // Act + var conventions = new DefaultLinkConventions(); + var relationshipLink = conventions.GetRelationshipLink(relationshipOwner, mockRegistry.Object, relationshipProperty, "https://www.example.com"); + + // Assert + relationshipLink.Href.Should().Be("https://www.example.com/foo/45/bar"); + } + + [TestMethod] + public void GetRelatedResourceLink_returns_default_url_for_relationship() + { + // Arrange + var relationshipOwner = new Country { Id = "45" }; + var relationshipProperty = new ToManyResourceTypeRelationship(typeof(Country).GetProperty("Cities"), + "cities", typeof(City), null, null); + var mockTypeRegistration = new Mock(MockBehavior.Strict); + mockTypeRegistration.Setup(r => r.ResourceTypeName).Returns("countries"); + mockTypeRegistration.Setup(r => r.GetIdForResource(relationshipOwner)).Returns("45"); + var mockRegistry = new Mock(MockBehavior.Strict); + mockRegistry.Setup(m => m.GetRegistrationForType(typeof(Country))).Returns(mockTypeRegistration.Object); + + // Act + var conventions = new DefaultLinkConventions(); + var relationshipLink = conventions.GetRelatedResourceLink(relationshipOwner, mockRegistry.Object, relationshipProperty, "https://www.example.com"); + + // Assert + relationshipLink.Href.Should().Be("https://www.example.com/countries/45/cities"); + } + + [TestMethod] + public void GetRelatedResourceLink_returns_default_url_for_relationship_when_base_url_has_trailing_slash() + { + // Arrange + var relationshipOwner = new Country { Id = "45" }; + var relationshipProperty = new ToManyResourceTypeRelationship(typeof(Country).GetProperty("Cities"), + "cities", typeof(City), null, null); + var mockTypeRegistration = new Mock(MockBehavior.Strict); + mockTypeRegistration.Setup(r => r.ResourceTypeName).Returns("countries"); + mockTypeRegistration.Setup(r => r.GetIdForResource(relationshipOwner)).Returns("45"); + var mockRegistry = new Mock(MockBehavior.Strict); + mockRegistry.Setup(m => m.GetRegistrationForType(typeof(Country))).Returns(mockTypeRegistration.Object); + + // Act + var conventions = new DefaultLinkConventions(); + var relationshipLink = conventions.GetRelatedResourceLink(relationshipOwner, mockRegistry.Object, relationshipProperty, "https://www.example.com"); + + // Assert + relationshipLink.Href.Should().Be("https://www.example.com/countries/45/cities"); + } + + [TestMethod] + public void GetRelatedResourceLink_is_correct_if_template_is_present() + { + // Arrange + var relationshipOwner = new Country { Id = "45" }; + var relationshipProperty = new ToManyResourceTypeRelationship(typeof(Country).GetProperty("Cities"), + "cities", typeof(City), null, "bar/{1}/qux"); + var mockTypeRegistration = new Mock(MockBehavior.Strict); + mockTypeRegistration.Setup(r => r.GetIdForResource(relationshipOwner)).Returns("45"); + var mockRegistry = new Mock(MockBehavior.Strict); + mockRegistry.Setup(m => m.GetRegistrationForType(typeof(Country))).Returns(mockTypeRegistration.Object); + + // Act + var conventions = new DefaultLinkConventions(); + var relationshipLink = conventions.GetRelatedResourceLink(relationshipOwner, mockRegistry.Object, relationshipProperty, "https://www.example.com"); + + // Assert + relationshipLink.Href.Should().Be("https://www.example.com/bar/45/qux"); + } + + [TestMethod] + public void GetRelatedResourceLink_is_correct_if_template_is_present_and_base_url_has_trailing_slash() + { + // Arrange + var relationshipOwner = new Country { Id = "45" }; + var relationshipProperty = new ToManyResourceTypeRelationship(typeof(Country).GetProperty("Cities"), + "cities", typeof(City), null, "bar/{1}/qux"); + var mockTypeRegistration = new Mock(MockBehavior.Strict); + mockTypeRegistration.Setup(r => r.GetIdForResource(relationshipOwner)).Returns("45"); + var mockRegistry = new Mock(MockBehavior.Strict); + mockRegistry.Setup(m => m.GetRegistrationForType(typeof(Country))).Returns(mockTypeRegistration.Object); + + // Act + var conventions = new DefaultLinkConventions(); + var relationshipLink = conventions.GetRelatedResourceLink(relationshipOwner, mockRegistry.Object, relationshipProperty, "https://www.example.com"); + + // Assert + relationshipLink.Href.Should().Be("https://www.example.com/bar/45/qux"); + } + + } +} diff --git a/JSONAPI.Tests/Documents/ToManyResourceLinkageTests.cs b/JSONAPI.Tests/Documents/ToManyResourceLinkageTests.cs new file mode 100644 index 00000000..80196eba --- /dev/null +++ b/JSONAPI.Tests/Documents/ToManyResourceLinkageTests.cs @@ -0,0 +1,47 @@ +using FluentAssertions; +using JSONAPI.Documents; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace JSONAPI.Tests.Documents +{ + [TestClass] + public class ToManyResourceLinkageTests + { + [TestMethod] + public void Identifiers_is_correct_for_present_identifiers() + { + var mockIdentifier1 = new Mock(MockBehavior.Strict); + mockIdentifier1.Setup(i => i.Type).Returns("countries"); + mockIdentifier1.Setup(i => i.Id).Returns("1000"); + + var mockIdentifier2 = new Mock(MockBehavior.Strict); + mockIdentifier2.Setup(i => i.Type).Returns("cities"); + mockIdentifier2.Setup(i => i.Id).Returns("4000"); + + var linkage = new ToManyResourceLinkage(new [] { mockIdentifier1.Object, mockIdentifier2.Object }); + + linkage.Identifiers.Length.Should().Be(2); + linkage.Identifiers[0].Type.Should().Be("countries"); + linkage.Identifiers[0].Id.Should().Be("1000"); + linkage.Identifiers[1].Type.Should().Be("cities"); + linkage.Identifiers[1].Id.Should().Be("4000"); + } + + [TestMethod] + public void Returns_corrent_LinkageToken_for_null_identifiers() + { + var linkage = new ToManyResourceLinkage(null); + + linkage.Identifiers.Length.Should().Be(0); + } + + [TestMethod] + public void Returns_corrent_LinkageToken_for_empty_identifiers() + { + var linkage = new ToManyResourceLinkage(new IResourceIdentifier[] { }); + + linkage.Identifiers.Length.Should().Be(0); + } + } +} \ No newline at end of file diff --git a/JSONAPI.Tests/Documents/ToOneResourceLinkageTests.cs b/JSONAPI.Tests/Documents/ToOneResourceLinkageTests.cs new file mode 100644 index 00000000..7c524612 --- /dev/null +++ b/JSONAPI.Tests/Documents/ToOneResourceLinkageTests.cs @@ -0,0 +1,34 @@ +using System.Linq; +using FluentAssertions; +using JSONAPI.Documents; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace JSONAPI.Tests.Documents +{ + [TestClass] + public class ToOneResourceLinkageTests + { + [TestMethod] + public void Identifiers_is_correct_for_present_identifier() + { + var mockIdentifier = new Mock(MockBehavior.Strict); + mockIdentifier.Setup(i => i.Type).Returns("countries"); + mockIdentifier.Setup(i => i.Id).Returns("1000"); + + var linkage = new ToOneResourceLinkage(mockIdentifier.Object); + + linkage.Identifiers.Length.Should().Be(1); + linkage.Identifiers.First().Type.Should().Be("countries"); + linkage.Identifiers.First().Id.Should().Be("1000"); + } + + [TestMethod] + public void Identifiers_is_correct_for_missing_identifier() + { + var linkage = new ToOneResourceLinkage(null); + + linkage.Identifiers.Length.Should().Be(0); + } + } +} \ No newline at end of file diff --git a/JSONAPI.Tests/Extensions/TypeExtensionsTests.cs b/JSONAPI.Tests/Extensions/TypeExtensionsTests.cs index 19b51b69..9d593abc 100644 --- a/JSONAPI.Tests/Extensions/TypeExtensionsTests.cs +++ b/JSONAPI.Tests/Extensions/TypeExtensionsTests.cs @@ -1,6 +1,7 @@ using System; using JSONAPI.Extensions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; namespace JSONAPI.Tests.Extensions { @@ -46,6 +47,9 @@ public void CanWriteAsJsonApiAttributeTest() Assert.IsTrue(typeof(String).CanWriteAsJsonApiAttribute(), "CanWriteTypeAsAttribute returned wrong answer for String!"); Assert.IsTrue(typeof(TestEnum).CanWriteAsJsonApiAttribute(), "CanWriteTypeAsAttribute returned wrong answer for enum!"); Assert.IsTrue(typeof(TestEnum?).CanWriteAsJsonApiAttribute(), "CanWriteTypeAsAttribute returned wrong answer for nullable enum!"); + Assert.IsTrue(typeof(JToken).CanWriteAsJsonApiAttribute(), "CanWriteTypeAsAttribute returned wrong answer for JToken!"); + Assert.IsTrue(typeof(JObject).CanWriteAsJsonApiAttribute(), "CanWriteTypeAsAttribute returned wrong answer for JObject!"); + Assert.IsTrue(typeof(JArray).CanWriteAsJsonApiAttribute(), "CanWriteTypeAsAttribute returned wrong answer for Jarray!"); Assert.IsFalse(typeof(Object).CanWriteAsJsonApiAttribute(), "CanWriteTypeAsAttribute returned wrong answer for Object!"); } 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/Http/DefaultIncludeExpressionExtractorTests.cs b/JSONAPI.Tests/Http/DefaultIncludeExpressionExtractorTests.cs new file mode 100644 index 00000000..0fb65380 --- /dev/null +++ b/JSONAPI.Tests/Http/DefaultIncludeExpressionExtractorTests.cs @@ -0,0 +1,57 @@ +using System.Net.Http; +using FluentAssertions; +using JSONAPI.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Tests.Http +{ + [TestClass] + public class DefaultIncludeExpressionExtractorTests + { + [TestMethod] + public void ExtractsSingleIncludeExpressionFromUri() + { + // Arrange + const string uri = "http://api.example.com/dummies?include=boss"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultIncludeExpressionExtractor(); + var inclExpressions = extractor.ExtractIncludeExpressions(request); + + // Assert + inclExpressions.Should().BeEquivalentTo("boss"); + } + + + [TestMethod] + public void ExtractsMultipleIncludeExpressionsFromUri() + { + // Arrange + const string uri = "http://api.example.com/dummies?include=boss,office-address"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultIncludeExpressionExtractor(); + var inclExpressions = extractor.ExtractIncludeExpressions(request); + + // Assert + inclExpressions.Should().BeEquivalentTo("boss", "office-address"); + } + + [TestMethod] + public void ExtractsNothingWhenThereIsNoIncludeParam() + { + // Arrange + const string uri = "http://api.example.com/dummies"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultIncludeExpressionExtractor(); + var inclExpression = extractor.ExtractIncludeExpressions(request); + + // Assert + inclExpression.Length.Should().Be(0); + } + } +} diff --git a/JSONAPI.Tests/Http/DefaultSortExpressionExtractorTests.cs b/JSONAPI.Tests/Http/DefaultSortExpressionExtractorTests.cs new file mode 100644 index 00000000..85b6003b --- /dev/null +++ b/JSONAPI.Tests/Http/DefaultSortExpressionExtractorTests.cs @@ -0,0 +1,86 @@ +using System.Net.Http; +using FluentAssertions; +using JSONAPI.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Tests.Http +{ + [TestClass] + public class DefaultSortExpressionExtractorTests + { + [TestMethod] + public void ExtractsSingleSortExpressionFromUri() + { + // Arrange + const string uri = "http://api.example.com/dummies?sort=first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultSortExpressionExtractor(); + var sortExpressions = extractor.ExtractSortExpressions(request); + + // Assert + sortExpressions.Should().BeEquivalentTo("first-name"); + } + + [TestMethod] + public void ExtractsSingleDescendingSortExpressionFromUri() + { + // Arrange + const string uri = "http://api.example.com/dummies?sort=-first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultSortExpressionExtractor(); + var sortExpressions = extractor.ExtractSortExpressions(request); + + // Assert + sortExpressions.Should().BeEquivalentTo("-first-name"); + } + + [TestMethod] + public void ExtractsMultipleSortExpressionsFromUri() + { + // Arrange + const string uri = "http://api.example.com/dummies?sort=last-name,first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultSortExpressionExtractor(); + var sortExpressions = extractor.ExtractSortExpressions(request); + + // Assert + sortExpressions.Should().BeEquivalentTo("last-name", "first-name"); + } + + [TestMethod] + public void ExtractsMultipleSortExpressionsFromUriWithDifferentDirections() + { + // Arrange + const string uri = "http://api.example.com/dummies?sort=last-name,-first-name"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultSortExpressionExtractor(); + var sortExpressions = extractor.ExtractSortExpressions(request); + + // Assert + sortExpressions.Should().BeEquivalentTo("last-name", "-first-name"); + } + + [TestMethod] + public void ExtractsNothingWhenThereIsNoSortParam() + { + // Arrange + const string uri = "http://api.example.com/dummies"; + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // Act + var extractor = new DefaultSortExpressionExtractor(); + var sortExpressions = extractor.ExtractSortExpressions(request); + + // Assert + sortExpressions.Length.Should().Be(0); + } + } +} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 5b292f1d..7f487003 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -22,7 +22,7 @@ true full false - C:\temp\JSONAPI.Tests\bin\Debug\ + bin\Debug\ DEBUG;TRACE prompt 4 @@ -79,21 +79,52 @@ - - - + + + + + + + + + + - - - + + + + + - + + + + + + + + + + + + + + + + + + + + + {0fe799ec-b6c5-499b-b56c-b97613342f6c} + JSONAPI.Tests.SingleControllerWebApp + {52b19fd6-efaa-45b5-9c3e-a652e27608d1} JSONAPI @@ -101,38 +132,62 @@ - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +