Skip to content

Commit 0839529

Browse files
author
Bart Koelman
committed
Added tests for archiving
1 parent b36e333 commit 0839529

13 files changed

+1020
-0
lines changed

JsonApiDotNetCore.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,4 +626,5 @@ $left$ = $right$;</s:String>
626626
<s:Boolean x:Key="/Default/UserDictionary/Words/=Rewriter/@EntryIndexedValue">True</s:Boolean>
627627
<s:Boolean x:Key="/Default/UserDictionary/Words/=Startups/@EntryIndexedValue">True</s:Boolean>
628628
<s:Boolean x:Key="/Default/UserDictionary/Words/=subdirectory/@EntryIndexedValue">True</s:Boolean>
629+
<s:Boolean x:Key="/Default/UserDictionary/Words/=unarchive/@EntryIndexedValue">True</s:Boolean>
629630
</wpf:ResourceDictionary>

test/JsonApiDotNetCoreExampleTests/IntegrationTests/Archiving/ArchiveTests.cs

Lines changed: 612 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
using JetBrains.Annotations;
3+
using JsonApiDotNetCore.Resources;
4+
using JsonApiDotNetCore.Resources.Annotations;
5+
6+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Archiving
7+
{
8+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
9+
public sealed class BroadcastComment : Identifiable
10+
{
11+
[Attr]
12+
public string Text { get; set; }
13+
14+
[Attr]
15+
public DateTimeOffset CreatedAt { get; set; }
16+
17+
[HasOne]
18+
public TelevisionBroadcast AppliesTo { get; set; }
19+
}
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using JsonApiDotNetCore.Configuration;
2+
using JsonApiDotNetCore.Controllers;
3+
using JsonApiDotNetCore.Services;
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Archiving
7+
{
8+
public sealed class BroadcastCommentsController : JsonApiController<BroadcastComment>
9+
{
10+
public BroadcastCommentsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService<BroadcastComment> resourceService)
11+
: base(options, loggerFactory, resourceService)
12+
{
13+
}
14+
}
15+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using JetBrains.Annotations;
4+
using JsonApiDotNetCore.Resources;
5+
using JsonApiDotNetCore.Resources.Annotations;
6+
7+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Archiving
8+
{
9+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
10+
public sealed class TelevisionBroadcast : Identifiable
11+
{
12+
[Attr]
13+
public string Title { get; set; }
14+
15+
[Attr]
16+
public DateTimeOffset AiredAt { get; set; }
17+
18+
[Attr]
19+
public DateTimeOffset? ArchivedAt { get; set; }
20+
21+
[HasOne]
22+
public TelevisionStation AiredOn { get; set; }
23+
24+
[HasMany]
25+
public ISet<BroadcastComment> Comments { get; set; }
26+
}
27+
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using JetBrains.Annotations;
8+
using JsonApiDotNetCore;
9+
using JsonApiDotNetCore.Configuration;
10+
using JsonApiDotNetCore.Errors;
11+
using JsonApiDotNetCore.Middleware;
12+
using JsonApiDotNetCore.Queries;
13+
using JsonApiDotNetCore.Queries.Expressions;
14+
using JsonApiDotNetCore.Resources;
15+
using JsonApiDotNetCore.Resources.Annotations;
16+
using JsonApiDotNetCore.Serialization.Objects;
17+
using Microsoft.EntityFrameworkCore;
18+
19+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Archiving
20+
{
21+
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
22+
public sealed class TelevisionBroadcastDefinition : JsonApiResourceDefinition<TelevisionBroadcast>
23+
{
24+
private readonly TelevisionDbContext _dbContext;
25+
private readonly IJsonApiRequest _request;
26+
private readonly IEnumerable<IQueryConstraintProvider> _constraintProviders;
27+
private readonly ResourceContext _broadcastContext;
28+
29+
private DateTimeOffset? _storedArchivedAt;
30+
31+
public TelevisionBroadcastDefinition(IResourceGraph resourceGraph, TelevisionDbContext dbContext, IJsonApiRequest request,
32+
IEnumerable<IQueryConstraintProvider> constraintProviders)
33+
: base(resourceGraph)
34+
{
35+
_dbContext = dbContext;
36+
_request = request;
37+
_constraintProviders = constraintProviders;
38+
_broadcastContext = resourceGraph.GetResourceContext<TelevisionBroadcast>();
39+
}
40+
41+
public override FilterExpression OnApplyFilter(FilterExpression existingFilter)
42+
{
43+
if (_request.IsReadOnly)
44+
{
45+
// Rule: hide archived broadcasts in collections, unless a filter is specified.
46+
47+
if (IsReturningCollectionOfTelevisionBroadcasts() && !HasFilterOnArchivedAt(existingFilter))
48+
{
49+
AttrAttribute archivedAtAttribute =
50+
_broadcastContext.Attributes.Single(attr => attr.Property.Name == nameof(TelevisionBroadcast.ArchivedAt));
51+
52+
var archivedAtChain = new ResourceFieldChainExpression(archivedAtAttribute);
53+
54+
FilterExpression isUnarchived = new ComparisonExpression(ComparisonOperator.Equals, archivedAtChain, new NullConstantExpression());
55+
56+
return existingFilter == null
57+
? isUnarchived
58+
: new LogicalExpression(LogicalOperator.And, ArrayFactory.Create(existingFilter, isUnarchived));
59+
}
60+
}
61+
62+
return base.OnApplyFilter(existingFilter);
63+
}
64+
65+
private bool IsReturningCollectionOfTelevisionBroadcasts()
66+
{
67+
return IsRequestingCollectionOfTelevisionBroadcasts() || IsIncludingCollectionOfTelevisionBroadcasts();
68+
}
69+
70+
private bool IsRequestingCollectionOfTelevisionBroadcasts()
71+
{
72+
if (_request.IsCollection)
73+
{
74+
if (_request.PrimaryResource == _broadcastContext || _request.SecondaryResource == _broadcastContext)
75+
{
76+
return true;
77+
}
78+
}
79+
80+
return false;
81+
}
82+
83+
private bool IsIncludingCollectionOfTelevisionBroadcasts()
84+
{
85+
// @formatter:wrap_chained_method_calls chop_always
86+
// @formatter:keep_existing_linebreaks true
87+
88+
IncludeElementExpression[] includeElements = _constraintProviders
89+
.SelectMany(provider => provider.GetConstraints())
90+
.Select(expressionInScope => expressionInScope.Expression)
91+
.OfType<IncludeExpression>()
92+
.SelectMany(include => include.Elements)
93+
.ToArray();
94+
95+
// @formatter:keep_existing_linebreaks restore
96+
// @formatter:wrap_chained_method_calls restore
97+
98+
foreach (IncludeElementExpression includeElement in includeElements)
99+
{
100+
if (includeElement.Relationship is HasManyAttribute && includeElement.Relationship.RightType == _broadcastContext.ResourceType)
101+
{
102+
return true;
103+
}
104+
}
105+
106+
return false;
107+
}
108+
109+
private bool HasFilterOnArchivedAt(FilterExpression existingFilter)
110+
{
111+
if (existingFilter == null)
112+
{
113+
return false;
114+
}
115+
116+
var walker = new FilterWalker();
117+
walker.Visit(existingFilter, null);
118+
119+
return walker.HasFilterOnArchivedAt;
120+
}
121+
122+
public override Task OnBeforeCreateResourceAsync(TelevisionBroadcast broadcast, CancellationToken cancellationToken)
123+
{
124+
AssertIsNotArchived(broadcast);
125+
126+
return base.OnBeforeCreateResourceAsync(broadcast, cancellationToken);
127+
}
128+
129+
[AssertionMethod]
130+
private static void AssertIsNotArchived(TelevisionBroadcast broadcast)
131+
{
132+
if (broadcast.ArchivedAt != null)
133+
{
134+
throw new JsonApiException(new Error(HttpStatusCode.Forbidden)
135+
{
136+
Title = "Television broadcasts cannot be created in archived state."
137+
});
138+
}
139+
}
140+
141+
public override Task OnAfterGetForUpdateResourceAsync(TelevisionBroadcast resource, CancellationToken cancellationToken)
142+
{
143+
_storedArchivedAt = resource.ArchivedAt;
144+
145+
return base.OnAfterGetForUpdateResourceAsync(resource, cancellationToken);
146+
}
147+
148+
public override Task OnBeforeUpdateResourceAsync(TelevisionBroadcast resource, CancellationToken cancellationToken)
149+
{
150+
AssertIsNotShiftingArchiveDate(resource);
151+
152+
return base.OnBeforeUpdateResourceAsync(resource, cancellationToken);
153+
}
154+
155+
[AssertionMethod]
156+
private void AssertIsNotShiftingArchiveDate(TelevisionBroadcast resource)
157+
{
158+
if (_storedArchivedAt != null && resource.ArchivedAt != null && _storedArchivedAt != resource.ArchivedAt)
159+
{
160+
throw new JsonApiException(new Error(HttpStatusCode.Forbidden)
161+
{
162+
Title = "Archive date of television broadcasts cannot be shifted. Unarchive it first."
163+
});
164+
}
165+
}
166+
167+
public override async Task OnBeforeDeleteResourceAsync(int broadcastId, CancellationToken cancellationToken)
168+
{
169+
TelevisionBroadcast televisionBroadcast =
170+
await _dbContext.Broadcasts.FirstOrDefaultAsync(broadcast => broadcast.Id == broadcastId, cancellationToken);
171+
172+
if (televisionBroadcast != null)
173+
{
174+
AssertIsArchived(televisionBroadcast);
175+
}
176+
177+
await base.OnBeforeDeleteResourceAsync(broadcastId, cancellationToken);
178+
}
179+
180+
[AssertionMethod]
181+
private static void AssertIsArchived(TelevisionBroadcast televisionBroadcast)
182+
{
183+
if (televisionBroadcast.ArchivedAt == null)
184+
{
185+
throw new JsonApiException(new Error(HttpStatusCode.Forbidden)
186+
{
187+
Title = "Television broadcasts must first be archived before they can be deleted."
188+
});
189+
}
190+
}
191+
192+
private sealed class FilterWalker : QueryExpressionRewriter<object>
193+
{
194+
public bool HasFilterOnArchivedAt { get; private set; }
195+
196+
public override QueryExpression VisitResourceFieldChain(ResourceFieldChainExpression expression, object argument)
197+
{
198+
if (expression.Fields.First().Property.Name == nameof(TelevisionBroadcast.ArchivedAt))
199+
{
200+
HasFilterOnArchivedAt = true;
201+
}
202+
203+
return base.VisitResourceFieldChain(expression, argument);
204+
}
205+
}
206+
}
207+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using JsonApiDotNetCore.Configuration;
2+
using JsonApiDotNetCore.Controllers;
3+
using JsonApiDotNetCore.Services;
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Archiving
7+
{
8+
public sealed class TelevisionBroadcastsController : JsonApiController<TelevisionBroadcast>
9+
{
10+
public TelevisionBroadcastsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService<TelevisionBroadcast> resourceService)
11+
: base(options, loggerFactory, resourceService)
12+
{
13+
}
14+
}
15+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using JetBrains.Annotations;
2+
using Microsoft.EntityFrameworkCore;
3+
4+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Archiving
5+
{
6+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
7+
public sealed class TelevisionDbContext : DbContext
8+
{
9+
public DbSet<TelevisionNetwork> Networks { get; set; }
10+
public DbSet<TelevisionStation> Stations { get; set; }
11+
public DbSet<TelevisionBroadcast> Broadcasts { get; set; }
12+
public DbSet<BroadcastComment> Comments { get; set; }
13+
14+
public TelevisionDbContext(DbContextOptions<TelevisionDbContext> options)
15+
: base(options)
16+
{
17+
}
18+
}
19+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using Bogus;
3+
using TestBuildingBlocks;
4+
5+
// @formatter:wrap_chained_method_calls chop_always
6+
// @formatter:keep_existing_linebreaks true
7+
8+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Archiving
9+
{
10+
internal sealed class TelevisionFakers : FakerContainer
11+
{
12+
private readonly Lazy<Faker<TelevisionNetwork>> _lazyTelevisionNetworkFaker = new Lazy<Faker<TelevisionNetwork>>(() =>
13+
new Faker<TelevisionNetwork>()
14+
.UseSeed(GetFakerSeed())
15+
.RuleFor(network => network.Name, faker => faker.Company.CompanyName()));
16+
17+
private readonly Lazy<Faker<TelevisionStation>> _lazyTelevisionStationFaker = new Lazy<Faker<TelevisionStation>>(() =>
18+
new Faker<TelevisionStation>()
19+
.UseSeed(GetFakerSeed())
20+
.RuleFor(station => station.Name, faker => faker.Company.CompanyName()));
21+
22+
private readonly Lazy<Faker<TelevisionBroadcast>> _lazyTelevisionBroadcastFaker = new Lazy<Faker<TelevisionBroadcast>>(() =>
23+
new Faker<TelevisionBroadcast>()
24+
.UseSeed(GetFakerSeed())
25+
.RuleFor(broadcast => broadcast.Title, faker => faker.Lorem.Sentence())
26+
.RuleFor(broadcast => broadcast.AiredAt, faker => faker.Date.PastOffset())
27+
.RuleFor(broadcast => broadcast.ArchivedAt, faker => faker.Date.RecentOffset()));
28+
29+
private readonly Lazy<Faker<BroadcastComment>> _lazyBroadcastCommentFaker = new Lazy<Faker<BroadcastComment>>(() =>
30+
new Faker<BroadcastComment>()
31+
.UseSeed(GetFakerSeed())
32+
.RuleFor(comment => comment.Text, faker => faker.Lorem.Paragraph())
33+
.RuleFor(comment => comment.CreatedAt, faker => faker.Date.PastOffset()));
34+
35+
public Faker<TelevisionNetwork> TelevisionNetwork => _lazyTelevisionNetworkFaker.Value;
36+
public Faker<TelevisionStation> TelevisionStation => _lazyTelevisionStationFaker.Value;
37+
public Faker<TelevisionBroadcast> TelevisionBroadcast => _lazyTelevisionBroadcastFaker.Value;
38+
public Faker<BroadcastComment> BroadcastComment => _lazyBroadcastCommentFaker.Value;
39+
}
40+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Collections.Generic;
2+
using JetBrains.Annotations;
3+
using JsonApiDotNetCore.Resources;
4+
using JsonApiDotNetCore.Resources.Annotations;
5+
6+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Archiving
7+
{
8+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
9+
public sealed class TelevisionNetwork : Identifiable
10+
{
11+
[Attr]
12+
public string Name { get; set; }
13+
14+
[HasMany]
15+
public ISet<TelevisionStation> Stations { get; set; }
16+
}
17+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using JsonApiDotNetCore.Configuration;
2+
using JsonApiDotNetCore.Controllers;
3+
using JsonApiDotNetCore.Services;
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Archiving
7+
{
8+
public sealed class TelevisionNetworksController : JsonApiController<TelevisionNetwork>
9+
{
10+
public TelevisionNetworksController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService<TelevisionNetwork> resourceService)
11+
: base(options, loggerFactory, resourceService)
12+
{
13+
}
14+
}
15+
}

0 commit comments

Comments
 (0)