Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for adding todoitems #530

Merged
merged 1 commit into from
Apr 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Adding support for adding todoitems
Added more endpoint tests
  • Loading branch information
ardalis committed Apr 9, 2023
commit dc9ef35b69cfffb0e09c6c863f1d9d141ec2ce6a
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<PackageVersion Include="FastEndpoints.ApiExplorer" Version="2.0.1" />
<PackageVersion Include="FastEndpoints.Swagger" Version="5.5.0" />
<PackageVersion Include="FastEndpoints.Swagger.Swashbuckle" Version="2.0.1" />
<PackageVersion Include="FluentAssertions" Version="6.10.0" />
<PackageVersion Include="MediatR" Version="12.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.4" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Ardalis.Specification;
using Clean.Architecture.Core.ContributorAggregate;

namespace Clean.Architecture.Core.ProjectAggregate.Specifications;
namespace Clean.Architecture.Core.ContributorAggregate.Specifications;

public class ContributorByIdSpec : Specification<Contributor>, ISingleResultSpecification
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using Ardalis.Result;
using Ardalis.Result;

namespace Clean.Architecture.Core.Interfaces;

public interface IDeleteContributorService
{
public Task<Result> DeleteContributor(int contributorId);
// This service and method exist to provide a place in which to fire domain events
// when deleting this aggregate root entity
public Task<Result> DeleteContributor(int contributorId);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Clean.Architecture.Core.ContributorAggregate;
using Clean.Architecture.Core.ProjectAggregate.Specifications;
using Clean.Architecture.Core.ContributorAggregate.Specifications;
using Clean.Architecture.SharedKernel.Interfaces;
using FastEndpoints;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;

namespace Clean.Architecture.Web.Endpoints.ProjectEndpoints;

public class CreateToDoItemRequest
{
public const string Route = "/Projects/{ProjectId:int}/ToDoItems";
public static string BuildRoute(int projectId) => Route.Replace("{ProjectId:int}", projectId.ToString());

[Required]
[FromRoute]
public int ProjectId { get; set; }

[Required]
public string? Title { get; set; }
public string? Description { get; set; }
public int? ContributorId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Ardalis.ApiEndpoints;
using Clean.Architecture.Core.ProjectAggregate;
using Clean.Architecture.Core.ProjectAggregate.Specifications;
using Clean.Architecture.SharedKernel.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;

namespace Clean.Architecture.Web.Endpoints.ProjectEndpoints;

public class CreateToDoItem : EndpointBaseAsync
.WithRequest<CreateToDoItemRequest>
.WithActionResult
{
private readonly IRepository<Project> _repository;

public CreateToDoItem(IRepository<Project> repository)
{
_repository = repository;
}

[HttpPost(CreateToDoItemRequest.Route)]
[SwaggerOperation(
Summary = "Creates a new ToDo Item for a Project",
Description = "Creates a new ToDo Item for a Project",
OperationId = "Project.CreateToDoItem",
Tags = new[] { "ProjectEndpoints" })
]
public override async Task<ActionResult> HandleAsync(
CreateToDoItemRequest request,
CancellationToken cancellationToken = new())
{
var spec = new ProjectByIdWithItemsSpec(request.ProjectId);
var entity = await _repository.FirstOrDefaultAsync(spec, cancellationToken);
if (entity == null)
{
return NotFound();
}

var newItem = new ToDoItem()
{
Title = request.Title!,
Description = request.Description!
};

if(request.ContributorId.HasValue)
{
newItem.AddContributor(request.ContributorId.Value);
}
entity.AddItem(newItem);
await _repository.UpdateAsync(entity);

return Created(GetProjectByIdRequest.BuildRoute(request.ProjectId), null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ public override async Task<ActionResult<GetProjectByIdResponse>> HandleAsync(
(
id: entity.Id,
name: entity.Name,
items: entity.Items.Select(item => new ToDoItemRecord(item.Id, item.Title, item.Description, item.IsDone))
items: entity.Items.Select(
item => new ToDoItemRecord(item.Id,
item.Title,
item.Description,
item.IsDone,
item.ContributorId))
.ToList()
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public override async Task<ActionResult<ListIncompleteResponse>> HandleAsync(
item => new ToDoItemRecord(item.Id,
item.Title,
item.Description,
item.IsDone)));
item.IsDone,
item.ContributorId)));
}
else if (result.Status == Ardalis.Result.ResultStatus.Invalid)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
namespace Clean.Architecture.Web.Endpoints.ProjectEndpoints;

public record ToDoItemRecord(int Id, string Title, string Description, bool IsDone);
public record ToDoItemRecord(int Id, string Title, string Description, bool IsDone, int? ContributorId);
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public Update(IRepository<Project> repository)
[HttpPut(UpdateProjectRequest.Route)]
[SwaggerOperation(
Summary = "Updates a Project",
Description = "Updates a Project with a longer description",
Description = "Updates a Project. Only supports changing the name.",
OperationId = "Projects.Update",
Tags = new[] { "ProjectEndpoints" })
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Ardalis.HttpClientTestExtensions;
using Clean.Architecture.Web.Endpoints.ProjectEndpoints;
using Xunit;
using FluentAssertions;
using Clean.Architecture.Web;

namespace Clean.Architecture.FunctionalTests.ApiEndpoints;

[Collection("Sequential")]
public class ProjectCreate : IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly HttpClient _client;

public ProjectCreate(CustomWebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}

[Fact]
public async Task ReturnsOneProject()
{
string testName = Guid.NewGuid().ToString();
var request = new CreateProjectRequest() { Name = testName };
var content = StringContentHelpers.FromModelAsJson(request);

var result = await _client.PostAndDeserializeAsync<CreateProjectResponse>(
CreateProjectRequest.Route, content);

result.Name.Should().Be(testName);
result.Id.Should().BeGreaterThan(0);
}
}

[Collection("Sequential")]
public class ProjectAddToDoItem : IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly HttpClient _client;

public ProjectAddToDoItem(CustomWebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}

[Fact]
public async Task AddsItemAndReturnsRouteToProject()
{
string toDoTitle = Guid.NewGuid().ToString();
int testProjectId = SeedData.TestProject1.Id;
var request = new CreateToDoItemRequest() {
Title = toDoTitle,
ProjectId = testProjectId,
Description = toDoTitle
};
var content = StringContentHelpers.FromModelAsJson(request);

var result = await _client.PostAsync(CreateToDoItemRequest.BuildRoute(testProjectId), content);

// useful for debugging error responses:
// var stringContent = await result.Content.ReadAsStringAsync();

string expectedRoute = GetProjectByIdRequest.BuildRoute(testProjectId);
result.Headers.Location!.ToString().Should().Be(expectedRoute);

var updatedProject = await _client.GetAndDeserializeAsync<GetProjectByIdResponse>(expectedRoute);

updatedProject.Items.Should().ContainSingle(item => item.Title == toDoTitle);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Ardalis.HttpClientTestExtensions;
using Clean.Architecture.Core.ProjectAggregate;
using Clean.Architecture.Web;
using Clean.Architecture.Web.Endpoints.ProjectEndpoints;
using Xunit;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
Expand Down