Skip to content

Commit 9491922

Browse files
Add unit tests for service classes, request handlers, API controllers, and Telegram commands (#39)
* Initial plan * Add comprehensive unit tests for service classes with MIT-licensed packages Co-authored-by: thomasneuberger <23504477+thomasneuberger@users.noreply.github.com> * Add unit tests for Request Handlers, API Controllers, and Telegram Commands Co-authored-by: thomasneuberger <23504477+thomasneuberger@users.noreply.github.com> * Run CI jobs in parallel - remove dependency between test and build Co-authored-by: thomasneuberger <23504477+thomasneuberger@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thomasneuberger <23504477+thomasneuberger@users.noreply.github.com>
1 parent 361b7fa commit 9491922

22 files changed

Lines changed: 1868 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@ on: [ push, workflow_dispatch ]
44

55
jobs:
66

7+
test:
8+
runs-on: ubuntu-latest
9+
10+
steps:
11+
- uses: actions/checkout@v4
12+
13+
- name: Setup .NET
14+
uses: actions/setup-dotnet@v4
15+
with:
16+
dotnet-version: '8.0.x'
17+
18+
- name: Restore dependencies
19+
run: dotnet restore
20+
21+
- name: Build
22+
run: dotnet build --no-restore
23+
24+
- name: Test
25+
run: dotnet test --no-build --verbosity normal
26+
727
build:
828

929
runs-on: ubuntu-latest

COPILOT_INSTRUCTIONS_TEMPLATE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ public static class Bootstrap
139139

140140
- **Central Package Management**: All package versions defined in `Directory.Packages.props`
141141
- Use `<PackageReference>` without version attributes in project files
142+
- **MIT License Requirement**: Only use external packages with MIT license to ensure compliance and compatibility
142143
- Key dependencies:
143144
- **MediatR** - CQRS pattern
144145
- **Serilog.AspNetCore** - Structured logging

Directory.Packages.props

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,10 @@
1818
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
1919
<PackageVersion Include="Serilog.Expressions" Version="4.0.0" />
2020
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.4.0" />
21+
<PackageVersion Include="NUnit" Version="4.2.2" />
22+
<PackageVersion Include="NUnit3TestAdapter" Version="4.6.0" />
23+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
24+
<PackageVersion Include="NSubstitute" Version="5.3.0" />
25+
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.11" />
2126
</ItemGroup>
2227
</Project>
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
using MediatR;
2+
using Microsoft.AspNetCore.Mvc;
3+
using NSubstitute;
4+
using NUnit.Framework;
5+
using TgHomeBot.Api.Controllers;
6+
using TgHomeBot.Notifications.Contract.Requests;
7+
using TgHomeBot.Notifications.Telegram.Models;
8+
using TgHomeBot.Notifications.Telegram.Services;
9+
10+
namespace TgHomeBot.Api.Tests.Controllers;
11+
12+
[TestFixture]
13+
public class NotificationControllerTests
14+
{
15+
private IMediator _mediator = null!;
16+
private IRegisteredChatService _registeredChatService = null!;
17+
private NotificationController _controller = null!;
18+
19+
[SetUp]
20+
public void SetUp()
21+
{
22+
_mediator = Substitute.For<IMediator>();
23+
_registeredChatService = Substitute.For<IRegisteredChatService>();
24+
_controller = new NotificationController(_mediator, _registeredChatService);
25+
}
26+
27+
[Test]
28+
public async Task SendMessage_ShouldSendNotifyRequest_AndReturnOk()
29+
{
30+
// Arrange
31+
const string message = "Test notification";
32+
_mediator.Send(Arg.Any<NotifyRequest>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(Unit.Value));
33+
34+
// Act
35+
var result = await _controller.SendMessage(message);
36+
37+
// Assert
38+
Assert.That(result, Is.InstanceOf<OkResult>());
39+
await _mediator.Received(1).Send(Arg.Is<NotifyRequest>(r => r.Message == message), Arg.Any<CancellationToken>());
40+
}
41+
42+
[Test]
43+
public void GetRegisteredChats_ShouldReturnAllChats()
44+
{
45+
// Arrange
46+
var chats = new List<RegisteredChat>
47+
{
48+
new() { Id = 1, Username = "user1", ChatId = 100, ChatName = "Chat 1" },
49+
new() { Id = 2, Username = "user2", ChatId = 200, ChatName = "Chat 2" }
50+
}.AsReadOnly();
51+
_registeredChatService.RegisteredChats.Returns(chats);
52+
53+
// Act
54+
var result = _controller.GetRegisteredChats();
55+
56+
// Assert
57+
Assert.That(result.Result, Is.InstanceOf<OkObjectResult>());
58+
var okResult = result.Result as OkObjectResult;
59+
Assert.That(okResult, Is.Not.Null);
60+
var returnedChats = okResult!.Value as IEnumerable<object>;
61+
Assert.That(returnedChats, Is.Not.Null);
62+
Assert.That(returnedChats!.Count(), Is.EqualTo(2));
63+
}
64+
65+
[Test]
66+
public void GetChatFlags_ShouldReturnFlags_WhenChatExists()
67+
{
68+
// Arrange
69+
const long chatId = 100;
70+
var chat = new RegisteredChat
71+
{
72+
Id = 1,
73+
Username = "user1",
74+
ChatId = chatId,
75+
ChatName = "Chat 1",
76+
EurojackpotEnabled = true,
77+
MonthlyChargingReportEnabled = false,
78+
DeviceNotificationsEnabled = true
79+
};
80+
_registeredChatService.GetRegisteredChat(chatId).Returns(chat);
81+
82+
// Act
83+
var result = _controller.GetChatFlags(chatId);
84+
85+
// Assert
86+
Assert.That(result.Result, Is.InstanceOf<OkObjectResult>());
87+
}
88+
89+
[Test]
90+
public void GetChatFlags_ShouldReturnNotFound_WhenChatDoesNotExist()
91+
{
92+
// Arrange
93+
const long chatId = 999;
94+
_registeredChatService.GetRegisteredChat(chatId).Returns((RegisteredChat?)null);
95+
96+
// Act
97+
var result = _controller.GetChatFlags(chatId);
98+
99+
// Assert
100+
Assert.That(result.Result, Is.InstanceOf<NotFoundObjectResult>());
101+
}
102+
103+
[Test]
104+
public async Task ToggleEurojackpot_ShouldToggleFlag_AndReturnSuccess()
105+
{
106+
// Arrange
107+
const long chatId = 100;
108+
_registeredChatService.ToggleEurojackpotAsync(chatId).Returns(Task.FromResult(true));
109+
var chat = new RegisteredChat
110+
{
111+
Id = 1,
112+
Username = "user1",
113+
ChatId = chatId,
114+
EurojackpotEnabled = false
115+
};
116+
_registeredChatService.GetRegisteredChat(chatId).Returns(chat);
117+
118+
// Act
119+
var result = await _controller.ToggleEurojackpot(chatId);
120+
121+
// Assert
122+
Assert.That(result.Result, Is.InstanceOf<OkObjectResult>());
123+
var okResult = result.Result as OkObjectResult;
124+
Assert.That(okResult, Is.Not.Null);
125+
await _registeredChatService.Received(1).ToggleEurojackpotAsync(chatId);
126+
}
127+
128+
[Test]
129+
public async Task ToggleEurojackpot_ShouldReturnNotFound_WhenChatDoesNotExist()
130+
{
131+
// Arrange
132+
const long chatId = 999;
133+
_registeredChatService.ToggleEurojackpotAsync(chatId).Returns(Task.FromResult(false));
134+
135+
// Act
136+
var result = await _controller.ToggleEurojackpot(chatId);
137+
138+
// Assert
139+
Assert.That(result.Result, Is.InstanceOf<NotFoundObjectResult>());
140+
}
141+
142+
[Test]
143+
public async Task ToggleMonthlyReport_ShouldToggleFlag_AndReturnSuccess()
144+
{
145+
// Arrange
146+
const long chatId = 100;
147+
_registeredChatService.ToggleMonthlyChargingReportAsync(chatId).Returns(Task.FromResult(true));
148+
var chat = new RegisteredChat
149+
{
150+
Id = 1,
151+
Username = "user1",
152+
ChatId = chatId,
153+
MonthlyChargingReportEnabled = true
154+
};
155+
_registeredChatService.GetRegisteredChat(chatId).Returns(chat);
156+
157+
// Act
158+
var result = await _controller.ToggleMonthlyReport(chatId);
159+
160+
// Assert
161+
Assert.That(result.Result, Is.InstanceOf<OkObjectResult>());
162+
await _registeredChatService.Received(1).ToggleMonthlyChargingReportAsync(chatId);
163+
}
164+
165+
[Test]
166+
public async Task ToggleDeviceNotifications_ShouldToggleFlag_AndReturnSuccess()
167+
{
168+
// Arrange
169+
const long chatId = 100;
170+
_registeredChatService.ToggleDeviceNotificationsAsync(chatId).Returns(Task.FromResult(true));
171+
var chat = new RegisteredChat
172+
{
173+
Id = 1,
174+
Username = "user1",
175+
ChatId = chatId,
176+
DeviceNotificationsEnabled = false
177+
};
178+
_registeredChatService.GetRegisteredChat(chatId).Returns(chat);
179+
180+
// Act
181+
var result = await _controller.ToggleDeviceNotifications(chatId);
182+
183+
// Assert
184+
Assert.That(result.Result, Is.InstanceOf<OkObjectResult>());
185+
await _registeredChatService.Received(1).ToggleDeviceNotificationsAsync(chatId);
186+
}
187+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using Microsoft.Extensions.Logging;
3+
using NSubstitute;
4+
using NUnit.Framework;
5+
using TgHomeBot.Api.Controllers;
6+
using TgHomeBot.Api.Models;
7+
using TgHomeBot.Scheduling.Contract;
8+
using TgHomeBot.Scheduling.Contract.Models;
9+
10+
namespace TgHomeBot.Api.Tests.Controllers;
11+
12+
[TestFixture]
13+
public class SchedulerControllerTests
14+
{
15+
private ILogger<SchedulerController> _logger = null!;
16+
private ISchedulerService _schedulerService = null!;
17+
private SchedulerController _controller = null!;
18+
19+
[SetUp]
20+
public void SetUp()
21+
{
22+
_logger = Substitute.For<ILogger<SchedulerController>>();
23+
_schedulerService = Substitute.For<ISchedulerService>();
24+
_controller = new SchedulerController(_logger, _schedulerService);
25+
}
26+
27+
[Test]
28+
public void GetScheduledTasks_ShouldReturnTasks()
29+
{
30+
// Arrange
31+
var tasks = new List<ScheduledTaskInfo>
32+
{
33+
new() { TaskType = "Task1", TaskName = "Task One", CronExpression = "0 0 * * *", Enabled = true },
34+
new() { TaskType = "Task2", TaskName = "Task Two", CronExpression = "0 12 * * *", Enabled = false }
35+
};
36+
_schedulerService.GetScheduledTasks().Returns(tasks);
37+
38+
// Act
39+
var result = _controller.GetScheduledTasks();
40+
41+
// Assert
42+
Assert.That(result.Result, Is.InstanceOf<OkObjectResult>());
43+
var okResult = result.Result as OkObjectResult;
44+
Assert.That(okResult, Is.Not.Null);
45+
var returnedTasks = okResult!.Value as List<ScheduledTaskInfo>;
46+
Assert.That(returnedTasks, Is.Not.Null);
47+
Assert.That(returnedTasks!.Count, Is.EqualTo(2));
48+
}
49+
50+
[Test]
51+
public void GetScheduledTasks_ShouldReturnEmptyList_WhenNoTasks()
52+
{
53+
// Arrange
54+
_schedulerService.GetScheduledTasks().Returns(new List<ScheduledTaskInfo>());
55+
56+
// Act
57+
var result = _controller.GetScheduledTasks();
58+
59+
// Assert
60+
Assert.That(result.Result, Is.InstanceOf<OkObjectResult>());
61+
var okResult = result.Result as OkObjectResult;
62+
var returnedTasks = okResult!.Value as List<ScheduledTaskInfo>;
63+
Assert.That(returnedTasks, Is.Empty);
64+
}
65+
66+
[Test]
67+
public async Task RunTaskNow_ShouldReturnOk_WhenTaskRunsSuccessfully()
68+
{
69+
// Arrange
70+
var request = new RunTaskRequest { TaskType = "TestTask" };
71+
_schedulerService.RunTaskNowAsync("TestTask").Returns(Task.FromResult(true));
72+
73+
// Act
74+
var result = await _controller.RunTaskNow(request);
75+
76+
// Assert
77+
Assert.That(result, Is.InstanceOf<OkObjectResult>());
78+
await _schedulerService.Received(1).RunTaskNowAsync("TestTask");
79+
}
80+
81+
[Test]
82+
public async Task RunTaskNow_ShouldReturnBadRequest_WhenTaskFails()
83+
{
84+
// Arrange
85+
var request = new RunTaskRequest { TaskType = "FailingTask" };
86+
_schedulerService.RunTaskNowAsync("FailingTask").Returns(Task.FromResult(false));
87+
88+
// Act
89+
var result = await _controller.RunTaskNow(request);
90+
91+
// Assert
92+
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
93+
await _schedulerService.Received(1).RunTaskNowAsync("FailingTask");
94+
}
95+
96+
[Test]
97+
public async Task RunTaskNow_ShouldReturnBadRequest_WhenTaskTypeIsNull()
98+
{
99+
// Arrange
100+
var request = new RunTaskRequest { TaskType = null! };
101+
102+
// Act
103+
var result = await _controller.RunTaskNow(request);
104+
105+
// Assert
106+
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
107+
await _schedulerService.DidNotReceive().RunTaskNowAsync(Arg.Any<string>());
108+
}
109+
110+
[Test]
111+
public async Task RunTaskNow_ShouldReturnBadRequest_WhenTaskTypeIsEmpty()
112+
{
113+
// Arrange
114+
var request = new RunTaskRequest { TaskType = "" };
115+
116+
// Act
117+
var result = await _controller.RunTaskNow(request);
118+
119+
// Assert
120+
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
121+
await _schedulerService.DidNotReceive().RunTaskNowAsync(Arg.Any<string>());
122+
}
123+
124+
[Test]
125+
public async Task RunTaskNow_ShouldReturnBadRequest_WhenRequestIsNull()
126+
{
127+
// Act
128+
var result = await _controller.RunTaskNow(null!);
129+
130+
// Assert
131+
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
132+
await _schedulerService.DidNotReceive().RunTaskNowAsync(Arg.Any<string>());
133+
}
134+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
<IsTestProject>true</IsTestProject>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
13+
<PackageReference Include="NUnit" />
14+
<PackageReference Include="NUnit3TestAdapter" />
15+
<PackageReference Include="NSubstitute" />
16+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<ProjectReference Include="..\TgHomeBot.Api\TgHomeBot.Api.csproj" />
21+
</ItemGroup>
22+
23+
</Project>

0 commit comments

Comments
 (0)