Skip to content

Commit ec5fa84

Browse files
authored
Merge pull request #357 from json-api-dotnet/fix/#340
Fix/#340: Operations issues
2 parents 6d2ccf5 + 8e065ce commit ec5fa84

File tree

6 files changed

+258
-62
lines changed

6 files changed

+258
-62
lines changed

Diff for: src/JsonApiDotNetCore/Builders/DocumentBuilder.cs

-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ public DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity, I
138138

139139
return data;
140140
}
141-
142141
private bool ShouldIncludeAttribute(AttrAttribute attr, object attributeValue)
143142
{
144143
return OmitNullValuedAttribute(attr, attributeValue) == false

Diff for: src/JsonApiDotNetCore/Services/Operations/OperationProcessorResolver.cs

+16-6
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ public IOpProcessor LocateCreateService(Operation operation)
5151
{
5252
var resource = operation.GetResourceTypeName();
5353

54-
var contextEntity = _context.ContextGraph.GetContextEntity(resource);
54+
var contextEntity = GetResourceMetadata(resource);
55+
5556
var processor = _processorFactory.GetProcessor<IOpProcessor>(
5657
typeof(ICreateOpProcessor<,>), contextEntity.EntityType, contextEntity.IdentityType
5758
);
@@ -64,7 +65,8 @@ public IOpProcessor LocateGetService(Operation operation)
6465
{
6566
var resource = operation.GetResourceTypeName();
6667

67-
var contextEntity = _context.ContextGraph.GetContextEntity(resource);
68+
var contextEntity = GetResourceMetadata(resource);
69+
6870
var processor = _processorFactory.GetProcessor<IOpProcessor>(
6971
typeof(IGetOpProcessor<,>), contextEntity.EntityType, contextEntity.IdentityType
7072
);
@@ -77,7 +79,8 @@ public IOpProcessor LocateRemoveService(Operation operation)
7779
{
7880
var resource = operation.GetResourceTypeName();
7981

80-
var contextEntity = _context.ContextGraph.GetContextEntity(resource);
82+
var contextEntity = GetResourceMetadata(resource);
83+
8184
var processor = _processorFactory.GetProcessor<IOpProcessor>(
8285
typeof(IRemoveOpProcessor<,>), contextEntity.EntityType, contextEntity.IdentityType
8386
);
@@ -90,15 +93,22 @@ public IOpProcessor LocateUpdateService(Operation operation)
9093
{
9194
var resource = operation.GetResourceTypeName();
9295

93-
var contextEntity = _context.ContextGraph.GetContextEntity(resource);
94-
if (contextEntity == null)
95-
throw new JsonApiException(400, $"This API does not expose a resource of type '{resource}'.");
96+
var contextEntity = GetResourceMetadata(resource);
9697

9798
var processor = _processorFactory.GetProcessor<IOpProcessor>(
9899
typeof(IUpdateOpProcessor<,>), contextEntity.EntityType, contextEntity.IdentityType
99100
);
100101

101102
return processor;
102103
}
104+
105+
private ContextEntity GetResourceMetadata(string resourceName)
106+
{
107+
var contextEntity = _context.ContextGraph.GetContextEntity(resourceName);
108+
if(contextEntity == null)
109+
throw new JsonApiException(400, $"This API does not expose a resource of type '{resourceName}'.");
110+
111+
return contextEntity;
112+
}
103113
}
104114
}

Diff for: src/JsonApiDotNetCore/Services/Operations/Processors/GetOpProcessor.cs

+34-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections;
12
using System.Collections.Generic;
23
using System.Linq;
34
using System.Threading.Tasks;
@@ -83,10 +84,10 @@ public async Task<Operation> ProcessAsync(Operation operation)
8384
};
8485

8586
operationResult.Data = string.IsNullOrWhiteSpace(operation.Ref.Id)
86-
? await GetAllAsync(operation)
87-
: string.IsNullOrWhiteSpace(operation.Ref.Relationship)
88-
? await GetByIdAsync(operation)
89-
: await GetRelationshipAsync(operation);
87+
? await GetAllAsync(operation)
88+
: string.IsNullOrWhiteSpace(operation.Ref.Relationship)
89+
? await GetByIdAsync(operation)
90+
: await GetRelationshipAsync(operation);
9091

9192
return operationResult;
9293
}
@@ -135,11 +136,38 @@ private async Task<object> GetRelationshipAsync(Operation operation)
135136
// when no generic parameter is available
136137
var relationshipType = _contextGraph.GetContextEntity(operation.GetResourceTypeName())
137138
.Relationships.Single(r => r.Is(operation.Ref.Relationship)).Type;
139+
138140
var relatedContextEntity = _jsonApiContext.ContextGraph.GetContextEntity(relationshipType);
139141

140-
var doc = _documentBuilder.GetData(relatedContextEntity, result as IIdentifiable); // TODO: if this is safe, then it should be cast in the GetRelationshipAsync call
142+
if (result == null)
143+
return null;
144+
145+
if (result is IIdentifiable singleResource)
146+
return GetData(relatedContextEntity, singleResource);
141147

142-
return doc;
148+
if (result is IEnumerable multipleResults)
149+
return GetData(relatedContextEntity, multipleResults);
150+
151+
throw new JsonApiException(500,
152+
$"An unexpected type was returned from '{_getRelationship.GetType()}.{nameof(IGetRelationshipService<T, TId>.GetRelationshipAsync)}'.",
153+
detail: $"Type '{result.GetType()} does not implement {nameof(IIdentifiable)} nor {nameof(IEnumerable<IIdentifiable>)}'");
154+
}
155+
156+
private DocumentData GetData(ContextEntity contextEntity, IIdentifiable singleResource)
157+
{
158+
return _documentBuilder.GetData(contextEntity, singleResource);
159+
}
160+
161+
private List<DocumentData> GetData(ContextEntity contextEntity, IEnumerable multipleResults)
162+
{
163+
var resources = new List<DocumentData>();
164+
foreach (var singleResult in multipleResults)
165+
{
166+
if (singleResult is IIdentifiable resource)
167+
resources.Add(_documentBuilder.GetData(contextEntity, resource));
168+
}
169+
170+
return resources;
143171
}
144172

145173
private TId GetReferenceId(Operation operation) => TypeHelper.ConvertType<TId>(operation.Ref.Id);

Diff for: test/OperationsExampleTests/Get/GetRelationshipTests.cs

+36-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class GetRelationshipTests : Fixture, IDisposable
1616
private readonly Faker _faker = new Faker();
1717

1818
[Fact]
19-
public async Task Can_Get_Article_Author()
19+
public async Task Can_Get_HasOne_Relationship()
2020
{
2121
// arrange
2222
var context = GetService<AppDbContext>();
@@ -48,5 +48,40 @@ public async Task Can_Get_Article_Author()
4848
Assert.Equal(author.Id.ToString(), resourceObject.Id);
4949
Assert.Equal("authors", resourceObject.Type);
5050
}
51+
52+
[Fact]
53+
public async Task Can_Get_HasMany_Relationship()
54+
{
55+
// arrange
56+
var context = GetService<AppDbContext>();
57+
var author = AuthorFactory.Get();
58+
var article = ArticleFactory.Get();
59+
article.Author = author;
60+
context.Articles.Add(article);
61+
context.SaveChanges();
62+
63+
var content = new
64+
{
65+
operations = new[] {
66+
new Dictionary<string, object> {
67+
{ "op", "get"},
68+
{ "ref", new { type = "authors", id = author.StringId, relationship = "articles" } }
69+
}
70+
}
71+
};
72+
73+
// act
74+
var (response, data) = await PatchAsync<OperationsDocument>("api/bulk", content);
75+
76+
// assert
77+
Assert.NotNull(response);
78+
Assert.NotNull(data);
79+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
80+
Assert.Single(data.Operations);
81+
82+
var resourceObject = data.Operations.Single().DataList.Single();
83+
Assert.Equal(article.Id.ToString(), resourceObject.Id);
84+
Assert.Equal("articles", resourceObject.Type);
85+
}
5186
}
5287
}

Diff for: test/OperationsExampleTests/Get/GetTests.cs

+70-48
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,73 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Net;
5-
using System.Threading.Tasks;
6-
using Bogus;
7-
using JsonApiDotNetCore.Models.Operations;
1+
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Net;
6+
using System.Threading.Tasks;
7+
using Bogus;
8+
using JsonApiDotNetCore.Models.Operations;
89
using JsonApiDotNetCoreExample.Data;
9-
using OperationsExampleTests.Factories;
10-
using Xunit;
11-
12-
namespace OperationsExampleTests
13-
{
14-
public class GetByIdTests : Fixture, IDisposable
15-
{
10+
using OperationsExampleTests.Factories;
11+
using Xunit;
12+
13+
namespace OperationsExampleTests
14+
{
15+
public class GetByIdTests : Fixture, IDisposable
16+
{
1617
private readonly Faker _faker = new Faker();
17-
18-
[Fact]
19-
public async Task Can_Get_Authors()
20-
{
21-
// arrange
22-
var expectedCount = _faker.Random.Int(1, 10);
23-
var context = GetService<AppDbContext>();
24-
context.Articles.RemoveRange(context.Articles);
25-
context.Authors.RemoveRange(context.Authors);
26-
var authors = AuthorFactory.Get(expectedCount);
27-
context.AddRange(authors);
28-
context.SaveChanges();
29-
30-
var content = new
31-
{
32-
operations = new[] {
33-
new Dictionary<string, object> {
34-
{ "op", "get"},
35-
{ "ref", new { type = "authors" } }
36-
}
37-
}
38-
};
39-
40-
// act
41-
var result = await PatchAsync<OperationsDocument>("api/bulk", content);
42-
43-
// assert
44-
Assert.NotNull(result.response);
45-
Assert.NotNull(result.data);
46-
Assert.Equal(HttpStatusCode.OK, result.response.StatusCode);
47-
Assert.Single(result.data.Operations);
48-
Assert.Equal(expectedCount, result.data.Operations.Single().DataList.Count);
18+
19+
[Fact]
20+
public async Task Can_Get_Authors()
21+
{
22+
// arrange
23+
var expectedCount = _faker.Random.Int(1, 10);
24+
var context = GetService<AppDbContext>();
25+
context.Articles.RemoveRange(context.Articles);
26+
context.Authors.RemoveRange(context.Authors);
27+
var authors = AuthorFactory.Get(expectedCount);
28+
context.AddRange(authors);
29+
context.SaveChanges();
30+
31+
var content = new
32+
{
33+
operations = new[] {
34+
new Dictionary<string, object> {
35+
{ "op", "get"},
36+
{ "ref", new { type = "authors" } }
37+
}
38+
}
39+
};
40+
41+
// act
42+
var result = await PatchAsync<OperationsDocument>("api/bulk", content);
43+
44+
// assert
45+
Assert.NotNull(result.response);
46+
Assert.NotNull(result.data);
47+
Assert.Equal(HttpStatusCode.OK, result.response.StatusCode);
48+
Assert.Single(result.data.Operations);
49+
Assert.Equal(expectedCount, result.data.Operations.Single().DataList.Count);
4950
}
50-
}
51-
}
51+
52+
[Fact]
53+
public async Task Get_Non_Existent_Type_Returns_400()
54+
{
55+
// arrange
56+
var content = new
57+
{
58+
operations = new[] {
59+
new Dictionary<string, object> {
60+
{ "op", "get"},
61+
{ "ref", new { type = "non-existent-type" } }
62+
}
63+
}
64+
};
65+
66+
// act
67+
var result = await PatchAsync<OperationsDocument>("api/bulk", content);
68+
69+
// assert
70+
Assert.Equal(HttpStatusCode.BadRequest, result.response.StatusCode);
71+
}
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using JsonApiDotNetCore.Builders;
2+
using JsonApiDotNetCore.Internal;
3+
using JsonApiDotNetCore.Internal.Generics;
4+
using JsonApiDotNetCore.Models.Operations;
5+
using JsonApiDotNetCore.Services;
6+
using JsonApiDotNetCore.Services.Operations;
7+
using Moq;
8+
using Xunit;
9+
10+
namespace UnitTests.Services
11+
{
12+
public class OperationProcessorResolverTests
13+
{
14+
private readonly Mock<IGenericProcessorFactory> _processorFactoryMock;
15+
public readonly Mock<IJsonApiContext> _jsonApiContextMock;
16+
17+
public OperationProcessorResolverTests()
18+
{
19+
_processorFactoryMock = new Mock<IGenericProcessorFactory>();
20+
_jsonApiContextMock = new Mock<IJsonApiContext>();
21+
}
22+
23+
[Fact]
24+
public void LocateCreateService_Throws_400_For_Entity_Not_Registered()
25+
{
26+
// arrange
27+
_jsonApiContextMock.Setup(m => m.ContextGraph).Returns(new ContextGraphBuilder().Build());
28+
var service = GetService();
29+
var op = new Operation
30+
{
31+
Ref = new ResourceReference
32+
{
33+
Type = "non-existent-type"
34+
}
35+
};
36+
37+
// act, assert
38+
var e = Assert.Throws<JsonApiException>(() => service.LocateCreateService(op));
39+
Assert.Equal(400, e.GetStatusCode());
40+
}
41+
42+
[Fact]
43+
public void LocateGetService_Throws_400_For_Entity_Not_Registered()
44+
{
45+
// arrange
46+
_jsonApiContextMock.Setup(m => m.ContextGraph).Returns(new ContextGraphBuilder().Build());
47+
var service = GetService();
48+
var op = new Operation
49+
{
50+
Ref = new ResourceReference
51+
{
52+
Type = "non-existent-type"
53+
}
54+
};
55+
56+
// act, assert
57+
var e = Assert.Throws<JsonApiException>(() => service.LocateGetService(op));
58+
Assert.Equal(400, e.GetStatusCode());
59+
}
60+
61+
[Fact]
62+
public void LocateRemoveService_Throws_400_For_Entity_Not_Registered()
63+
{
64+
// arrange
65+
_jsonApiContextMock.Setup(m => m.ContextGraph).Returns(new ContextGraphBuilder().Build());
66+
var service = GetService();
67+
var op = new Operation
68+
{
69+
Ref = new ResourceReference
70+
{
71+
Type = "non-existent-type"
72+
}
73+
};
74+
75+
// act, assert
76+
var e = Assert.Throws<JsonApiException>(() => service.LocateRemoveService(op));
77+
Assert.Equal(400, e.GetStatusCode());
78+
}
79+
80+
[Fact]
81+
public void LocateUpdateService_Throws_400_For_Entity_Not_Registered()
82+
{
83+
// arrange
84+
_jsonApiContextMock.Setup(m => m.ContextGraph).Returns(new ContextGraphBuilder().Build());
85+
var service = GetService();
86+
var op = new Operation
87+
{
88+
Ref = new ResourceReference
89+
{
90+
Type = "non-existent-type"
91+
}
92+
};
93+
94+
// act, assert
95+
var e = Assert.Throws<JsonApiException>(() => service.LocateUpdateService(op));
96+
Assert.Equal(400, e.GetStatusCode());
97+
}
98+
99+
private OperationProcessorResolver GetService()
100+
=> new OperationProcessorResolver(_processorFactoryMock.Object, _jsonApiContextMock.Object);
101+
}
102+
}

0 commit comments

Comments
 (0)