Skip to content

Commit efebf5e

Browse files
author
Bart Koelman
committed
Added stage transition tests
1 parent 8211d41 commit efebf5e

File tree

7 files changed

+349
-0
lines changed

7 files changed

+349
-0
lines changed

JsonApiDotNetCore.sln.DotSettings

+1
Original file line numberDiff line numberDiff line change
@@ -628,4 +628,5 @@ $left$ = $right$;</s:String>
628628
<s:Boolean x:Key="/Default/UserDictionary/Words/=Startups/@EntryIndexedValue">True</s:Boolean>
629629
<s:Boolean x:Key="/Default/UserDictionary/Words/=subdirectory/@EntryIndexedValue">True</s:Boolean>
630630
<s:Boolean x:Key="/Default/UserDictionary/Words/=unarchive/@EntryIndexedValue">True</s:Boolean>
631+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Workflows/@EntryIndexedValue">True</s:Boolean>
631632
</wpf:ResourceDictionary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
using JetBrains.Annotations;
3+
using JsonApiDotNetCore.Resources;
4+
using JsonApiDotNetCore.Resources.Annotations;
5+
6+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.InputValidation.RequestBody
7+
{
8+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
9+
public sealed class Workflow : Identifiable<Guid>
10+
{
11+
[Attr]
12+
public WorkflowStage Stage { get; set; }
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using JetBrains.Annotations;
2+
using Microsoft.EntityFrameworkCore;
3+
4+
// @formatter:wrap_chained_method_calls chop_always
5+
6+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.InputValidation.RequestBody
7+
{
8+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
9+
public sealed class WorkflowDbContext : DbContext
10+
{
11+
public DbSet<Workflow> Workflows { get; set; }
12+
13+
public WorkflowDbContext(DbContextOptions<WorkflowDbContext> options)
14+
: base(options)
15+
{
16+
}
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Net;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using JetBrains.Annotations;
7+
using JsonApiDotNetCore.Configuration;
8+
using JsonApiDotNetCore.Errors;
9+
using JsonApiDotNetCore.Middleware;
10+
using JsonApiDotNetCore.Resources;
11+
using JsonApiDotNetCore.Serialization.Objects;
12+
13+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.InputValidation.RequestBody
14+
{
15+
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
16+
public sealed class WorkflowDefinition : JsonApiResourceDefinition<Workflow, Guid>
17+
{
18+
private static readonly Dictionary<WorkflowStage, ICollection<WorkflowStage>> StageTransitionTable =
19+
new Dictionary<WorkflowStage, ICollection<WorkflowStage>>
20+
{
21+
[WorkflowStage.Created] = new[]
22+
{
23+
WorkflowStage.InProgress
24+
},
25+
[WorkflowStage.InProgress] = new[]
26+
{
27+
WorkflowStage.OnHold,
28+
WorkflowStage.Succeeded,
29+
WorkflowStage.Failed,
30+
WorkflowStage.Canceled
31+
},
32+
[WorkflowStage.OnHold] = new[]
33+
{
34+
WorkflowStage.InProgress,
35+
WorkflowStage.Canceled
36+
}
37+
};
38+
39+
private WorkflowStage _previousStage;
40+
41+
public WorkflowDefinition(IResourceGraph resourceGraph)
42+
: base(resourceGraph)
43+
{
44+
}
45+
46+
public override Task OnPrepareWriteAsync(Workflow resource, OperationKind operationKind, CancellationToken cancellationToken)
47+
{
48+
if (operationKind == OperationKind.UpdateResource)
49+
{
50+
_previousStage = resource.Stage;
51+
}
52+
53+
return Task.CompletedTask;
54+
}
55+
56+
[AssertionMethod]
57+
private static void AssertHasValidInitialStage(Workflow resource)
58+
{
59+
if (resource.Stage != WorkflowStage.Created)
60+
{
61+
throw new JsonApiException(new Error(HttpStatusCode.UnprocessableEntity)
62+
{
63+
Title = "Invalid workflow stage.",
64+
Detail = $"Initial stage of workflow must be '{WorkflowStage.Created}'.",
65+
Source =
66+
{
67+
Pointer = "/data/attributes/stage"
68+
}
69+
});
70+
}
71+
}
72+
73+
public override Task OnWritingAsync(Workflow resource, OperationKind operationKind, CancellationToken cancellationToken)
74+
{
75+
if (operationKind == OperationKind.CreateResource)
76+
{
77+
AssertHasValidInitialStage(resource);
78+
}
79+
else if (operationKind == OperationKind.UpdateResource && resource.Stage != _previousStage)
80+
{
81+
AssertCanTransitionToStage(_previousStage, resource.Stage);
82+
}
83+
84+
return Task.CompletedTask;
85+
}
86+
87+
[AssertionMethod]
88+
private static void AssertCanTransitionToStage(WorkflowStage fromStage, WorkflowStage toStage)
89+
{
90+
if (!CanTransitionToStage(fromStage, toStage))
91+
{
92+
throw new JsonApiException(new Error(HttpStatusCode.UnprocessableEntity)
93+
{
94+
Title = "Invalid workflow stage.",
95+
Detail = $"Cannot transition from '{fromStage}' to '{toStage}'.",
96+
Source =
97+
{
98+
Pointer = "/data/attributes/stage"
99+
}
100+
});
101+
}
102+
}
103+
104+
private static bool CanTransitionToStage(WorkflowStage fromStage, WorkflowStage toStage)
105+
{
106+
if (StageTransitionTable.ContainsKey(fromStage))
107+
{
108+
ICollection<WorkflowStage> possibleNextStages = StageTransitionTable[fromStage];
109+
return possibleNextStages.Contains(toStage);
110+
}
111+
112+
return false;
113+
}
114+
}
115+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.InputValidation.RequestBody
2+
{
3+
public enum WorkflowStage
4+
{
5+
Created,
6+
InProgress,
7+
OnHold,
8+
Succeeded,
9+
Failed,
10+
Canceled
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
using System.Net;
2+
using System.Net.Http;
3+
using System.Threading.Tasks;
4+
using FluentAssertions;
5+
using JsonApiDotNetCore.Configuration;
6+
using JsonApiDotNetCore.Serialization.Objects;
7+
using JsonApiDotNetCoreExampleTests.Startups;
8+
using TestBuildingBlocks;
9+
using Xunit;
10+
11+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.InputValidation.RequestBody
12+
{
13+
public sealed class WorkflowTests : IClassFixture<ExampleIntegrationTestContext<ModelStateValidationStartup<WorkflowDbContext>, WorkflowDbContext>>
14+
{
15+
private readonly ExampleIntegrationTestContext<ModelStateValidationStartup<WorkflowDbContext>, WorkflowDbContext> _testContext;
16+
17+
public WorkflowTests(ExampleIntegrationTestContext<ModelStateValidationStartup<WorkflowDbContext>, WorkflowDbContext> testContext)
18+
{
19+
_testContext = testContext;
20+
21+
testContext.UseController<WorkflowsController>();
22+
23+
testContext.ConfigureServicesAfterStartup(services =>
24+
{
25+
services.AddResourceDefinition<WorkflowDefinition>();
26+
});
27+
}
28+
29+
[Fact]
30+
public async Task Can_create_in_valid_stage()
31+
{
32+
// Arrange
33+
var requestBody = new
34+
{
35+
data = new
36+
{
37+
type = "workflows",
38+
attributes = new
39+
{
40+
stage = WorkflowStage.Created
41+
}
42+
}
43+
};
44+
45+
const string route = "/workflows";
46+
47+
// Act
48+
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync<Document>(route, requestBody);
49+
50+
// Assert
51+
httpResponse.Should().HaveStatusCode(HttpStatusCode.Created);
52+
53+
responseDocument.SingleData.Should().NotBeNull();
54+
}
55+
56+
[Fact]
57+
public async Task Cannot_create_in_invalid_stage()
58+
{
59+
// Arrange
60+
var requestBody = new
61+
{
62+
data = new
63+
{
64+
type = "workflows",
65+
attributes = new
66+
{
67+
stage = WorkflowStage.Canceled
68+
}
69+
}
70+
};
71+
72+
const string route = "/workflows";
73+
74+
// Act
75+
(HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync<ErrorDocument>(route, requestBody);
76+
77+
// Assert
78+
httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity);
79+
80+
responseDocument.Errors.Should().HaveCount(1);
81+
82+
Error error = responseDocument.Errors[0];
83+
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
84+
error.Title.Should().Be("Invalid workflow stage.");
85+
error.Detail.Should().Be("Initial stage of workflow must be 'Created'.");
86+
error.Source.Pointer.Should().Be("/data/attributes/stage");
87+
}
88+
89+
[Fact]
90+
public async Task Cannot_transition_to_invalid_stage()
91+
{
92+
// Arrange
93+
var existingWorkflow = new Workflow
94+
{
95+
Stage = WorkflowStage.OnHold
96+
};
97+
98+
await _testContext.RunOnDatabaseAsync(async dbContext =>
99+
{
100+
dbContext.Workflows.Add(existingWorkflow);
101+
await dbContext.SaveChangesAsync();
102+
});
103+
104+
var requestBody = new
105+
{
106+
data = new
107+
{
108+
type = "workflows",
109+
id = existingWorkflow.StringId,
110+
attributes = new
111+
{
112+
stage = WorkflowStage.Succeeded
113+
}
114+
}
115+
};
116+
117+
string route = "/workflows/" + existingWorkflow.StringId;
118+
119+
// Act
120+
(HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync<ErrorDocument>(route, requestBody);
121+
122+
// Assert
123+
httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity);
124+
125+
responseDocument.Errors.Should().HaveCount(1);
126+
127+
Error error = responseDocument.Errors[0];
128+
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
129+
error.Title.Should().Be("Invalid workflow stage.");
130+
error.Detail.Should().Be("Cannot transition from 'OnHold' to 'Succeeded'.");
131+
error.Source.Pointer.Should().Be("/data/attributes/stage");
132+
}
133+
134+
[Fact]
135+
public async Task Can_transition_to_valid_stage()
136+
{
137+
// Arrange
138+
var existingWorkflow = new Workflow
139+
{
140+
Stage = WorkflowStage.InProgress
141+
};
142+
143+
await _testContext.RunOnDatabaseAsync(async dbContext =>
144+
{
145+
dbContext.Workflows.Add(existingWorkflow);
146+
await dbContext.SaveChangesAsync();
147+
});
148+
149+
var requestBody = new
150+
{
151+
data = new
152+
{
153+
type = "workflows",
154+
id = existingWorkflow.StringId,
155+
attributes = new
156+
{
157+
stage = WorkflowStage.Failed
158+
}
159+
}
160+
};
161+
162+
string route = "/workflows/" + existingWorkflow.StringId;
163+
164+
// Act
165+
(HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync<string>(route, requestBody);
166+
167+
// Assert
168+
httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent);
169+
170+
responseDocument.Should().BeEmpty();
171+
}
172+
}
173+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Controllers;
4+
using JsonApiDotNetCore.Services;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.InputValidation.RequestBody
8+
{
9+
public sealed class WorkflowsController : JsonApiController<Workflow, Guid>
10+
{
11+
public WorkflowsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService<Workflow, Guid> resourceService)
12+
: base(options, loggerFactory, resourceService)
13+
{
14+
}
15+
}
16+
}

0 commit comments

Comments
 (0)