Skip to content

Commit cc3f11d

Browse files
authored
Merge pull request #30 from Research-Institute/spec-tests
Spec tests
2 parents d4bf48d + 5a5eb41 commit cc3f11d

File tree

11 files changed

+487
-28
lines changed

11 files changed

+487
-28
lines changed

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,16 @@ identifier):
206206
?filter[attribute]=ge:value
207207
```
208208

209+
210+
## Sorting
211+
212+
Resources can be sorted by an attribute:
213+
214+
```
215+
?sort=attribute // ascending
216+
?sort=-attribute // descending
217+
```
218+
209219
# Tests
210220

211221
## Running

src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using JsonApiDotNetCore.Middleware;
12
using Microsoft.AspNetCore.Builder;
23

34
namespace JsonApiDotNetCore.Routing
@@ -6,8 +7,10 @@ public static class IApplicationBuilderExtensions
67
{
78
public static IApplicationBuilder UseJsonApi(this IApplicationBuilder app)
89
{
10+
app.UseMiddleware<RequestMiddleware>();
11+
912
app.UseMvc();
10-
13+
1114
return app;
1215
}
1316
}

src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs

+29-12
Original file line numberDiff line numberDiff line change
@@ -34,38 +34,55 @@ public async Task WriteAsync(OutputFormatterWriteContext context)
3434
logger?.LogInformation("Formatting response as JSONAPI");
3535

3636
var response = context.HttpContext.Response;
37-
3837
using (var writer = context.WriterFactory(response.Body, Encoding.UTF8))
3938
{
4039
var jsonApiContext = GetService<IJsonApiContext>(context);
4140

41+
response.ContentType = "application/vnd.api+json";
4242
string responseContent;
4343
try
4444
{
45-
if(context.Object.GetType() == typeof(Error) || jsonApiContext.RequestEntity == null)
46-
{
47-
logger?.LogInformation("Response was not a JSONAPI entity. Serializing as plain JSON.");
48-
49-
responseContent = JsonConvert.SerializeObject(context.Object);
50-
}
51-
else
52-
responseContent = JsonApiSerializer.Serialize(context.Object, jsonApiContext);
45+
responseContent = GetResponseBody(context.Object, jsonApiContext, logger);
5346
}
54-
catch(Exception e)
47+
catch (Exception e)
5548
{
5649
logger?.LogError(new EventId(), e, "An error ocurred while formatting the response");
57-
responseContent = new Error("400", e.Message).GetJson();
50+
var errors = new ErrorCollection();
51+
errors.Add(new Error("400", e.Message));
52+
responseContent = errors.GetJson();
5853
response.StatusCode = 400;
5954
}
6055

6156
await writer.WriteAsync(responseContent);
62-
await writer.FlushAsync();
57+
await writer.FlushAsync();
6358
}
6459
}
6560

6661
private T GetService<T>(OutputFormatterWriteContext context)
6762
{
6863
return context.HttpContext.RequestServices.GetService<T>();
6964
}
65+
66+
private string GetResponseBody(object responseObject, IJsonApiContext jsonApiContext, ILogger logger)
67+
{
68+
if (responseObject.GetType() == typeof(Error) || jsonApiContext.RequestEntity == null)
69+
{
70+
if (responseObject.GetType() == typeof(Error))
71+
{
72+
var errors = new ErrorCollection();
73+
errors.Add((Error)responseObject);
74+
return errors.GetJson();
75+
}
76+
else
77+
{
78+
logger?.LogInformation("Response was not a JSONAPI entity. Serializing as plain JSON.");
79+
return JsonConvert.SerializeObject(responseObject);
80+
}
81+
}
82+
else
83+
{
84+
return JsonApiSerializer.Serialize(responseObject, jsonApiContext);
85+
}
86+
}
7087
}
7188
}

src/JsonApiDotNetCore/Internal/Error.cs

+3-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ namespace JsonApiDotNetCore.Internal
44
{
55
public class Error
66
{
7+
public Error()
8+
{ }
9+
710
public Error(string status, string title)
811
{
912
Status = status;
@@ -25,12 +28,5 @@ public Error(string status, string title, string detail)
2528

2629
[JsonProperty("status")]
2730
public string Status { get; set; }
28-
29-
public string GetJson()
30-
{
31-
return JsonConvert.SerializeObject(this, new JsonSerializerSettings {
32-
NullValueHandling = NullValueHandling.Ignore
33-
});
34-
}
3531
}
3632
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Collections.Generic;
2+
using Newtonsoft.Json;
3+
4+
namespace JsonApiDotNetCore.Internal
5+
{
6+
public class ErrorCollection
7+
{
8+
public ErrorCollection()
9+
{
10+
Errors = new List<Error>();
11+
}
12+
13+
public List<Error> Errors { get; set; }
14+
15+
public void Add(Error error)
16+
{
17+
Errors.Add(error);
18+
}
19+
20+
public string GetJson()
21+
{
22+
return JsonConvert.SerializeObject(this, new JsonSerializerSettings {
23+
NullValueHandling = NullValueHandling.Ignore
24+
});
25+
}
26+
}
27+
}

src/JsonApiDotNetCore/Internal/Query/QuerySet.cs

+4
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,16 @@ private void BuildQuerySet(IQueryCollection query)
4545
if (pair.Key.StartsWith("include"))
4646
{
4747
IncludedRelationships = ParseIncludedRelationships(pair.Value);
48+
continue;
4849
}
4950

5051
if (pair.Key.StartsWith("page"))
5152
{
5253
PageQuery = ParsePageQuery(pair.Key, pair.Value);
54+
continue;
5355
}
56+
57+
throw new JsonApiException("400", $"{pair} is not a valid query.");
5458
}
5559
}
5660

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System.Threading.Tasks;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.Extensions.Primitives;
4+
5+
namespace JsonApiDotNetCore.Middleware
6+
{
7+
public class RequestMiddleware
8+
{
9+
private readonly RequestDelegate _next;
10+
11+
public RequestMiddleware(RequestDelegate next)
12+
{
13+
_next = next;
14+
}
15+
16+
public async Task Invoke(HttpContext context)
17+
{
18+
if (IsValid(context))
19+
await _next(context);
20+
}
21+
22+
private static bool IsValid(HttpContext context)
23+
{
24+
return IsValidContentTypeHeader(context) && IsValidAcceptHeader(context);
25+
}
26+
27+
private static bool IsValidContentTypeHeader(HttpContext context)
28+
{
29+
var contentType = context.Request.ContentType;
30+
if (contentType != null && ContainsMediaTypeParameters(contentType))
31+
{
32+
FlushResponse(context, 415);
33+
return false;
34+
}
35+
return true;
36+
}
37+
38+
private static bool IsValidAcceptHeader(HttpContext context)
39+
{
40+
var acceptHeaders = new StringValues();
41+
if (context.Request.Headers.TryGetValue("Accept", out acceptHeaders))
42+
{
43+
foreach (var acceptHeader in acceptHeaders)
44+
{
45+
if (ContainsMediaTypeParameters(acceptHeader))
46+
{
47+
FlushResponse(context, 406);
48+
return false;
49+
}
50+
}
51+
}
52+
return true;
53+
}
54+
55+
private static bool ContainsMediaTypeParameters(string mediaType)
56+
{
57+
var mediaTypeArr = mediaType.Split(';');
58+
return (mediaTypeArr[0] == "application/vnd.api+json" && mediaTypeArr.Length == 2);
59+
}
60+
61+
private static void FlushResponse(HttpContext context, int statusCode)
62+
{
63+
context.Response.StatusCode = statusCode;
64+
context.Response.Body.Flush();
65+
}
66+
}
67+
}

src/JsonApiDotNetCore/project.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "0.2.8",
2+
"version": "0.2.9",
33

44
"dependencies": {
55
"Microsoft.NETCore.App": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System.Net;
2+
using System.Net.Http;
3+
using System.Net.Http.Headers;
4+
using System.Threading.Tasks;
5+
using DotNetCoreDocs;
6+
using DotNetCoreDocs.Models;
7+
using DotNetCoreDocs.Writers;
8+
using JsonApiDotNetCoreExample;
9+
using Microsoft.AspNetCore.Hosting;
10+
using Microsoft.AspNetCore.TestHost;
11+
using Xunit;
12+
13+
namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec
14+
{
15+
[Collection("WebHostCollection")]
16+
public class ContentNegotiation
17+
{
18+
private DocsFixture<Startup, JsonDocWriter> _fixture;
19+
public ContentNegotiation(DocsFixture<Startup, JsonDocWriter> fixture)
20+
{
21+
_fixture = fixture;
22+
}
23+
24+
[Fact]
25+
public async Task Server_Sends_Correct_ContentType_Header()
26+
{
27+
// arrange
28+
var builder = new WebHostBuilder()
29+
.UseStartup<Startup>();
30+
var httpMethod = new HttpMethod("GET");
31+
var route = "/api/v1/todo-items";
32+
var description = new RequestProperties("Server Sends Correct Content Type Header");
33+
var server = new TestServer(builder);
34+
var client = server.CreateClient();
35+
var request = new HttpRequestMessage(httpMethod, route);
36+
37+
// act
38+
var response = await client.SendAsync(request);
39+
40+
// assert
41+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
42+
Assert.Equal("application/vnd.api+json", response.Content.Headers.ContentType.ToString());
43+
}
44+
45+
[Fact]
46+
public async Task Server_Responds_415_With_MediaType_Parameters()
47+
{
48+
// arrange
49+
var builder = new WebHostBuilder()
50+
.UseStartup<Startup>();
51+
var httpMethod = new HttpMethod("GET");
52+
var route = "/api/v1/todo-items";
53+
var description = new RequestProperties("Server responds with 415 if request contains media type parameters");
54+
var server = new TestServer(builder);
55+
var client = server.CreateClient();
56+
var request = new HttpRequestMessage(httpMethod, route);
57+
request.Content = new StringContent(string.Empty);
58+
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
59+
request.Content.Headers.ContentType.CharSet = "ISO-8859-4";
60+
61+
// act
62+
var response = await client.SendAsync(request);
63+
64+
// assert
65+
Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode);
66+
}
67+
68+
[Fact]
69+
public async Task ServerResponds_406_If_RequestAcceptHeader_Contains_MediaTypeParameters()
70+
{
71+
// arrange
72+
var builder = new WebHostBuilder()
73+
.UseStartup<Startup>();
74+
var httpMethod = new HttpMethod("GET");
75+
var route = "/api/v1/todo-items";
76+
var description = new RequestProperties("Server responds with 406...");
77+
var server = new TestServer(builder);
78+
var client = server.CreateClient();
79+
var acceptHeader = new MediaTypeWithQualityHeaderValue("application/vnd.api+json");
80+
acceptHeader.CharSet = "ISO-8859-4";
81+
client.DefaultRequestHeaders
82+
.Accept
83+
.Add(acceptHeader);
84+
var request = new HttpRequestMessage(httpMethod, route);
85+
86+
// act
87+
var response = await client.SendAsync(request);
88+
89+
// assert
90+
Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode);
91+
}
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System.Net;
2+
using System.Net.Http;
3+
using System.Threading.Tasks;
4+
using DotNetCoreDocs;
5+
using DotNetCoreDocs.Models;
6+
using DotNetCoreDocs.Writers;
7+
using JsonApiDotNetCoreExample;
8+
using Microsoft.AspNetCore.Hosting;
9+
using Microsoft.AspNetCore.TestHost;
10+
using Newtonsoft.Json;
11+
using Xunit;
12+
using JsonApiDotNetCore.Internal;
13+
14+
namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec
15+
{
16+
[Collection("WebHostCollection")]
17+
public class QueryParameters
18+
{
19+
private DocsFixture<Startup, JsonDocWriter> _fixture;
20+
public QueryParameters(DocsFixture<Startup, JsonDocWriter> fixture)
21+
{
22+
_fixture = fixture;
23+
}
24+
25+
[Fact]
26+
public async Task Server_Returns_400_ForUnknownQueryParam()
27+
{
28+
// arrange
29+
const string queryKey = "unknownKey";
30+
const string queryValue = "value";
31+
var builder = new WebHostBuilder()
32+
.UseStartup<Startup>();
33+
var httpMethod = new HttpMethod("GET");
34+
var route = $"/api/v1/todo-items?{queryKey}={queryValue}";
35+
var description = new RequestProperties("Server Returns 400 For Unknown Query Params");
36+
var server = new TestServer(builder);
37+
var client = server.CreateClient();
38+
var request = new HttpRequestMessage(httpMethod, route);
39+
40+
// act
41+
var response = await client.SendAsync(request);
42+
var body = JsonConvert.DeserializeObject<ErrorCollection>(await response.Content.ReadAsStringAsync());
43+
44+
// assert
45+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
46+
Assert.Equal(1, body.Errors.Count);
47+
Assert.Equal($"[{queryKey}, {queryValue}] is not a valid query.", body.Errors[0].Title);
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)