Skip to content

Commit f14aab2

Browse files
committed
- Added more unit tests
- Setup Nuke for CI/CD Pipeline to run tests on every push
1 parent aaefdf0 commit f14aab2

21 files changed

Lines changed: 857 additions & 13 deletions

‎.github/workflows/CI.yml‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# ------------------------------------------------------------------------------
2+
# <auto-generated>
3+
#
4+
# This code was generated.
5+
#
6+
# - To turn off auto-generation set:
7+
#
8+
# [GitHubActions (AutoGenerate = false)]
9+
#
10+
# - To trigger manual generation invoke:
11+
#
12+
# nuke --generate-configuration GitHubActions_CI --host GitHubActions
13+
#
14+
# </auto-generated>
15+
# ------------------------------------------------------------------------------
16+
17+
name: CI
18+
19+
on: [push]
20+
21+
jobs:
22+
ubuntu-latest:
23+
name: ubuntu-latest
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v6
27+
- name: 'Cache: .nuke/temp, ~/.nuget/packages'
28+
uses: actions/cache@v4
29+
with:
30+
path: |
31+
.nuke/temp
32+
~/.nuget/packages
33+
key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }}
34+
- name: 'Run: Test'
35+
run: ./build.cmd Test

‎.nuke/build.schema.json‎

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"definitions": {
4+
"Host": {
5+
"type": "string",
6+
"enum": [
7+
"AppVeyor",
8+
"AzurePipelines",
9+
"Bamboo",
10+
"Bitbucket",
11+
"Bitrise",
12+
"GitHubActions",
13+
"GitLab",
14+
"Jenkins",
15+
"Rider",
16+
"SpaceAutomation",
17+
"TeamCity",
18+
"Terminal",
19+
"TravisCI",
20+
"VisualStudio",
21+
"VSCode"
22+
]
23+
},
24+
"ExecutableTarget": {
25+
"type": "string",
26+
"enum": [
27+
"Clean",
28+
"Compile",
29+
"Restore",
30+
"Test"
31+
]
32+
},
33+
"Verbosity": {
34+
"type": "string",
35+
"description": "",
36+
"enum": [
37+
"Verbose",
38+
"Normal",
39+
"Minimal",
40+
"Quiet"
41+
]
42+
},
43+
"NukeBuild": {
44+
"properties": {
45+
"Continue": {
46+
"type": "boolean",
47+
"description": "Indicates to continue a previously failed build attempt"
48+
},
49+
"Help": {
50+
"type": "boolean",
51+
"description": "Shows the help text for this build assembly"
52+
},
53+
"Host": {
54+
"description": "Host for execution. Default is 'automatic'",
55+
"$ref": "#/definitions/Host"
56+
},
57+
"NoLogo": {
58+
"type": "boolean",
59+
"description": "Disables displaying the NUKE logo"
60+
},
61+
"Partition": {
62+
"type": "string",
63+
"description": "Partition to use on CI"
64+
},
65+
"Plan": {
66+
"type": "boolean",
67+
"description": "Shows the execution plan (HTML)"
68+
},
69+
"Profile": {
70+
"type": "array",
71+
"description": "Defines the profiles to load",
72+
"items": {
73+
"type": "string"
74+
}
75+
},
76+
"Root": {
77+
"type": "string",
78+
"description": "Root directory during build execution"
79+
},
80+
"Skip": {
81+
"type": "array",
82+
"description": "List of targets to be skipped. Empty list skips all dependencies",
83+
"items": {
84+
"$ref": "#/definitions/ExecutableTarget"
85+
}
86+
},
87+
"Target": {
88+
"type": "array",
89+
"description": "List of targets to be invoked. Default is '{default_target}'",
90+
"items": {
91+
"$ref": "#/definitions/ExecutableTarget"
92+
}
93+
},
94+
"Verbosity": {
95+
"description": "Logging verbosity during build execution. Default is 'Normal'",
96+
"$ref": "#/definitions/Verbosity"
97+
}
98+
}
99+
}
100+
},
101+
"allOf": [
102+
{
103+
"properties": {
104+
"Configuration": {
105+
"type": "string",
106+
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
107+
"enum": [
108+
"Debug",
109+
"Release"
110+
]
111+
},
112+
"IgnoreFailedSources": {
113+
"type": "boolean"
114+
},
115+
"Solution": {
116+
"type": "string",
117+
"description": "Path to a solution file that is automatically loaded"
118+
}
119+
}
120+
},
121+
{
122+
"$ref": "#/definitions/NukeBuild"
123+
}
124+
]
125+
}

‎.nuke/parameters.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"$schema": "build.schema.json",
3+
"Solution": "TaskManagerAPI.sln"
4+
}

‎TaskManagerAPI.sln‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TasksService.IntegrationTes
5555
EndProject
5656
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TasksService.UnitTests", "TasksService.UnitTests\TasksService.UnitTests.csproj", "{92178ED4-0F75-6FCF-4907-52E99BE00FB8}"
5757
EndProject
58+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{130DA418-2208-4F97-A110-FB1514DB9A37}"
59+
EndProject
5860
Global
5961
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6062
Debug|Any CPU = Debug|Any CPU
6163
Release|Any CPU = Release|Any CPU
6264
EndGlobalSection
6365
GlobalSection(ProjectConfigurationPlatforms) = postSolution
66+
{130DA418-2208-4F97-A110-FB1514DB9A37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
67+
{130DA418-2208-4F97-A110-FB1514DB9A37}.Release|Any CPU.ActiveCfg = Release|Any CPU
6468
{A4690E08-75BA-4093-AF83-F1B26EA7F531}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
6569
{A4690E08-75BA-4093-AF83-F1B26EA7F531}.Debug|Any CPU.Build.0 = Debug|Any CPU
6670
{A4690E08-75BA-4093-AF83-F1B26EA7F531}.Release|Any CPU.ActiveCfg = Release|Any CPU

‎TasksService.Infrastructure/Repositories/TodoRepository.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Microsoft.EntityFrameworkCore; // CRITICAL: Needed for ToListAsync, FirstOrDefaultAsync
1+
using Microsoft.EntityFrameworkCore;
22
using TasksService.Domain.Interfaces;
33
using TasksService.Domain.Models;
44
using TasksService.Infrastructure.Data;

‎TasksService.IntegrationTests/TasksService.IntegrationTests.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
</ItemGroup>
1818

1919
<ItemGroup>
20-
<ProjectReference Include="..\TaskManagerAPI\TasksService.Api\TasksService.Api.csproj" />
20+
<ProjectReference Include="..\TasksService.Api\TasksService.Api.csproj" />
2121
</ItemGroup>
2222

2323
<ItemGroup>
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using AutoMapper;
2+
using FluentAssertions;
3+
using Microsoft.Extensions.Logging;
4+
using Moq;
5+
using TasksService.Application.DTOs;
6+
using TasksService.Application.Features.Commands.CreateTodo;
7+
using TasksService.Domain.Interfaces;
8+
using TasksService.Domain.Models;
9+
10+
namespace TasksService.UnitTests.Application.Features.Commands
11+
{
12+
public class CreateTodoCommandHandlerTests
13+
{
14+
private readonly Mock<ITodoRepository> _todoRepositoryMock;
15+
private readonly Mock<IMapper> _mapperMock;
16+
private readonly Mock<ILogger<CreateTodoCommandHandler>> _loggerMock;
17+
private readonly CreateTodoCommandHandler _handler;
18+
19+
public CreateTodoCommandHandlerTests()
20+
{
21+
_todoRepositoryMock = new Mock<ITodoRepository>();
22+
_mapperMock = new Mock<IMapper>();
23+
_loggerMock = new Mock<ILogger<CreateTodoCommandHandler>>();
24+
_handler = new CreateTodoCommandHandler(
25+
_todoRepositoryMock.Object,
26+
_loggerMock.Object,
27+
_mapperMock.Object
28+
);
29+
}
30+
31+
[Fact]
32+
public async Task Handle_ShouldReturnSuccess_WhenCreateTodoCommand()
33+
{
34+
// Arrange
35+
var command = new CreateTodoCommand
36+
{
37+
UserId = "1234",
38+
TodoRequest = new CreateTodoRequest
39+
{
40+
Title = "Test Todo",
41+
Description = "This is a test todo item."
42+
}
43+
};
44+
45+
var mappedTodo = new TodoItem
46+
{
47+
UserId = command.UserId,
48+
Title = command.TodoRequest.Title,
49+
Description = command.TodoRequest.Description
50+
};
51+
_mapperMock.Setup(x => x.Map<TodoItem>(command.TodoRequest))
52+
.Returns(mappedTodo);
53+
54+
var todoDto = new TodoDto
55+
{
56+
Id = 1,
57+
Title = command.TodoRequest.Title,
58+
Description = command.TodoRequest.Description,
59+
};
60+
_mapperMock.Setup(x => x.Map<TodoDto>(It.IsAny<TodoItem>()))
61+
.Returns(todoDto);
62+
_todoRepositoryMock.Setup(x => x.CreateTodoAsync(It.IsAny<TodoItem>()))
63+
.ReturnsAsync(mappedTodo);
64+
// Act
65+
var result = await _handler.Handle(command, CancellationToken.None);
66+
67+
// Assert
68+
result.IsSuccess.Should().BeTrue();
69+
}
70+
71+
[Fact]
72+
73+
public async Task Handle_ShouldReturnFailure_WhenRepositoryReturnsNull()
74+
{
75+
// Arrange
76+
var command = new CreateTodoCommand
77+
{
78+
UserId = "1234",
79+
TodoRequest = new CreateTodoRequest
80+
{
81+
Title = "X",
82+
Description = "Y"
83+
}
84+
};
85+
var todoDto = new TodoDto
86+
{
87+
Id = 1,
88+
Title = command.TodoRequest.Title,
89+
Description = command.TodoRequest.Description,
90+
};
91+
var mappedTodo = new TodoItem
92+
{
93+
UserId = command.UserId,
94+
Title = command.TodoRequest.Title,
95+
Description = command.TodoRequest.Description
96+
};
97+
98+
_mapperMock.Setup(x => x.Map<TodoItem>(command.TodoRequest))
99+
.Returns(mappedTodo);
100+
101+
_mapperMock.Setup(x => x.Map<TodoDto>(It.IsAny<TodoItem>()))
102+
.Returns(todoDto);
103+
104+
_todoRepositoryMock.Setup(x => x.CreateTodoAsync(It.IsAny<TodoItem>()))
105+
.ReturnsAsync((TodoItem?)null);
106+
107+
// Act
108+
var result = await _handler.Handle(command, CancellationToken.None);
109+
110+
// Assert
111+
result.IsSuccess.Should().BeFalse();
112+
}
113+
114+
}
115+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using Microsoft.Extensions.Logging;
2+
using Moq;
3+
using FluentAssertions;
4+
using TasksService.Application.Features.Commands.DeleteTodo;
5+
using TasksService.Domain.Interfaces;
6+
using TasksService.Domain.Models;
7+
8+
namespace TasksService.UnitTests.Application.Features.Commands
9+
{
10+
public class DeleteTodoCommandHandlerTests
11+
{
12+
private readonly Mock<ITodoRepository> _todoRepositoryMock;
13+
private readonly Mock<ILogger<DeleteTodoCommandHandler>> _loggerMock;
14+
private readonly DeleteTodoCommandHandler _handler;
15+
16+
public DeleteTodoCommandHandlerTests()
17+
{
18+
_todoRepositoryMock = new Mock<ITodoRepository>();
19+
_loggerMock = new Mock<ILogger<DeleteTodoCommandHandler>>();
20+
_handler = new DeleteTodoCommandHandler(
21+
_todoRepositoryMock.Object,
22+
_loggerMock.Object
23+
);
24+
}
25+
26+
[Fact]
27+
public async Task Handle_ShouldReturnSuccess_WhenTodoIsDeleted()
28+
{
29+
// Arrange
30+
var command = new DeleteTodoCommand
31+
{
32+
UserId = "1234",
33+
Id = 1
34+
};
35+
var existingTodo = new TodoItem
36+
{
37+
Id = command.Id,
38+
UserId = command.UserId,
39+
Title = "Test Todo",
40+
Description = "This is a test todo item."
41+
};
42+
43+
_todoRepositoryMock.Setup(x => x.GetTodoByIdAsync(command.UserId, command.Id))
44+
.ReturnsAsync(existingTodo);
45+
// Act
46+
var result = await _handler.Handle(command, CancellationToken.None);
47+
// Assert
48+
result.IsSuccess.Should().BeTrue();
49+
result.Value.Should().BeTrue();
50+
_todoRepositoryMock.Verify(x => x.DeleteTodoAsync(existingTodo), Times.Once);
51+
}
52+
[Fact]
53+
public async Task Handle_ShouldReturnFailure_WhenTodoNotFound()
54+
{
55+
// Arrange
56+
var command = new DeleteTodoCommand
57+
{
58+
UserId = "1234",
59+
Id = 1
60+
};
61+
_todoRepositoryMock.Setup(x => x.GetTodoByIdAsync(command.UserId, command.Id))
62+
.ReturnsAsync((TodoItem)null);
63+
// Act
64+
var result = await _handler.Handle(command, CancellationToken.None);
65+
// Assert
66+
result.IsSuccess.Should().BeFalse();
67+
result.Error.Should().Be("Todo item not found.");
68+
_todoRepositoryMock.Verify(x => x.DeleteTodoAsync(It.IsAny<TodoItem>()), Times.Never);
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)