diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj
new file mode 100644
index 0000000..100ed06
--- /dev/null
+++ b/API.Tests/API.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net9.0
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/API.Tests/Controllers/AgeGroupControllerTests.cs b/API.Tests/Controllers/AgeGroupControllerTests.cs
new file mode 100644
index 0000000..1bd444b
--- /dev/null
+++ b/API.Tests/Controllers/AgeGroupControllerTests.cs
@@ -0,0 +1,184 @@
+using API.Controllers;
+using API.Models.Internal.Altersgruppen;
+using API.Repository.AgeGroupRepo;
+using Microsoft.AspNetCore.Mvc;
+using Moq;
+
+namespace API.Tests.Controllers;
+
+public class AgeGroupControllerTests
+{
+ private readonly Mock _mockService;
+ private readonly AgeGroupController _controller;
+
+ public AgeGroupControllerTests()
+ {
+ _mockService = new Mock();
+ _controller = new AgeGroupController(_mockService.Object);
+ }
+
+ [Fact]
+ public async Task GetAll_ReturnsOkWithAllGroups()
+ {
+ // Arrange
+ var groups = new List
+ {
+ new AltersGruppe { Id = "1", Name = "U10", StartingAge = 8, EndingAge = 10 },
+ new AltersGruppe { Id = "2", Name = "U12", StartingAge = 10, EndingAge = 12 }
+ };
+ _mockService.Setup(s => s.GetAllAsync()).ReturnsAsync(groups);
+
+ // Act
+ var result = await _controller.GetAll();
+
+ // Assert
+ var okResult = Assert.IsType(result);
+ var returnedGroups = Assert.IsType>(okResult.Value);
+ Assert.Equal(2, returnedGroups.Count);
+ }
+
+ [Fact]
+ public async Task GetAll_EmptyList_ReturnsOkWithEmptyList()
+ {
+ // Arrange
+ _mockService.Setup(s => s.GetAllAsync()).ReturnsAsync(new List());
+
+ // Act
+ var result = await _controller.GetAll();
+
+ // Assert
+ var okResult = Assert.IsType(result);
+ var returnedGroups = Assert.IsType>(okResult.Value);
+ Assert.Empty(returnedGroups);
+ }
+
+ [Fact]
+ public async Task GetOne_ExistingId_ReturnsOkWithGroup()
+ {
+ // Arrange
+ var group = new AltersGruppe { Id = "123", Name = "U14", StartingAge = 12, EndingAge = 14 };
+ _mockService.Setup(s => s.GetAsync("123")).ReturnsAsync(group);
+
+ // Act
+ var result = await _controller.GetOne("123");
+
+ // Assert
+ var okResult = Assert.IsType(result);
+ var returnedGroup = Assert.IsType(okResult.Value);
+ Assert.Equal("U14", returnedGroup.Name);
+ }
+
+ [Fact]
+ public async Task GetOne_NonExistingId_ReturnsNotFound()
+ {
+ // Arrange
+ _mockService.Setup(s => s.GetAsync("nonexistent")).ReturnsAsync((AltersGruppe?)null);
+
+ // Act
+ var result = await _controller.GetOne("nonexistent");
+
+ // Assert
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public async Task Create_ValidGroup_ReturnsCreatedAtAction()
+ {
+ // Arrange
+ var ingoingGroup = new AltersGruppeIngoing { Name = "U16", StartingAge = 14, EndingAge = 16 };
+ var createdGroup = new AltersGruppe { Id = "new-id", Name = "U16", StartingAge = 14, EndingAge = 16 };
+
+ _mockService.Setup(s => s.CreateAsync(It.IsAny())).ReturnsAsync(createdGroup);
+
+ // Act
+ var result = await _controller.Create(ingoingGroup);
+
+ // Assert
+ var createdResult = Assert.IsType(result);
+ Assert.Equal(nameof(AgeGroupController.GetOne), createdResult.ActionName);
+
+ var returnedGroup = Assert.IsType(createdResult.Value);
+ Assert.Equal("U16", returnedGroup.Name);
+ Assert.Equal("new-id", createdResult.RouteValues?["Id"]);
+ }
+
+ [Fact]
+ public async Task Update_ExistingGroup_ReturnsOkWithUpdatedGroup()
+ {
+ // Arrange
+ var ingoingGroup = new AltersGruppeIngoing { Name = "U16 Updated", StartingAge = 15, EndingAge = 16 };
+ var updatedGroup = new AltersGruppe { Id = "123", Name = "U16 Updated", StartingAge = 15, EndingAge = 16 };
+
+ _mockService.Setup(s => s.UpdateAsync("123", It.IsAny())).ReturnsAsync(updatedGroup);
+
+ // Act
+ var result = await _controller.Update("123", ingoingGroup);
+
+ // Assert
+ var okResult = Assert.IsType(result);
+ var returnedGroup = Assert.IsType(okResult.Value);
+ Assert.Equal("U16 Updated", returnedGroup.Name);
+ Assert.Equal(15, returnedGroup.StartingAge);
+ }
+
+ [Fact]
+ public async Task Update_NonExistingGroup_ReturnsNotFound()
+ {
+ // Arrange
+ var ingoingGroup = new AltersGruppeIngoing { Name = "U16 Updated", StartingAge = 15, EndingAge = 16 };
+ _mockService.Setup(s => s.UpdateAsync("nonexistent", It.IsAny())).ReturnsAsync((AltersGruppe?)null);
+
+ // Act
+ var result = await _controller.Update("nonexistent", ingoingGroup);
+
+ // Assert
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public async Task Delete_ExistingGroup_ReturnsNoContent()
+ {
+ // Arrange
+ var group = new AltersGruppe { Id = "123", Name = "U18", StartingAge = 16, EndingAge = 18 };
+ _mockService.Setup(s => s.DeleteAsync("123")).ReturnsAsync(group);
+
+ // Act
+ var result = await _controller.Delete("123");
+
+ // Assert
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public async Task Delete_NonExistingGroup_ReturnsNotFound()
+ {
+ // Arrange
+ _mockService.Setup(s => s.DeleteAsync("nonexistent")).ReturnsAsync((AltersGruppe?)null);
+
+ // Act
+ var result = await _controller.Delete("nonexistent");
+
+ // Assert
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public async Task Create_ServiceIsCalled_WithCorrectParameters()
+ {
+ // Arrange
+ var ingoingGroup = new AltersGruppeIngoing { Name = "U20", StartingAge = 18, EndingAge = 20 };
+ var createdGroup = new AltersGruppe { Id = "new-id", Name = "U20", StartingAge = 18, EndingAge = 20 };
+
+ _mockService.Setup(s => s.CreateAsync(It.IsAny())).ReturnsAsync(createdGroup);
+
+ // Act
+ await _controller.Create(ingoingGroup);
+
+ // Assert
+ _mockService.Verify(s => s.CreateAsync(It.Is(g =>
+ g.Name == "U20" &&
+ g.StartingAge == 18 &&
+ g.EndingAge == 20
+ )), Times.Once);
+ }
+}
diff --git a/API.Tests/Mappers/AltersgruppeMapperTests.cs b/API.Tests/Mappers/AltersgruppeMapperTests.cs
new file mode 100644
index 0000000..2bfdff7
--- /dev/null
+++ b/API.Tests/Mappers/AltersgruppeMapperTests.cs
@@ -0,0 +1,118 @@
+using API.Models.Internal.Altersgruppen;
+
+namespace API.Tests.Mappers;
+
+public class AltersgruppeMapperTests
+{
+ [Fact]
+ public void ToInternalFromIngoing_MapsAllProperties()
+ {
+ // Arrange
+ var ingoing = new AltersGruppeIngoing
+ {
+ Name = "U14",
+ StartingAge = 12,
+ EndingAge = 14
+ };
+
+ // Act
+ var result = ingoing.ToInternalFromIngoing();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("U14", result.Name);
+ Assert.Equal(12, result.StartingAge);
+ Assert.Equal(14, result.EndingAge);
+ }
+
+ [Fact]
+ public void ToInternalFromIngoing_GeneratesNewId()
+ {
+ // Arrange
+ var ingoing = new AltersGruppeIngoing
+ {
+ Name = "U16",
+ StartingAge = 14,
+ EndingAge = 16
+ };
+
+ // Act
+ var result = ingoing.ToInternalFromIngoing();
+
+ // Assert
+ Assert.NotNull(result.Id);
+ Assert.NotEmpty(result.Id);
+ }
+
+ [Fact]
+ public void ToInternalFromIngoing_GeneratesUniqueIds()
+ {
+ // Arrange
+ var ingoing1 = new AltersGruppeIngoing { Name = "U10", StartingAge = 8, EndingAge = 10 };
+ var ingoing2 = new AltersGruppeIngoing { Name = "U12", StartingAge = 10, EndingAge = 12 };
+
+ // Act
+ var result1 = ingoing1.ToInternalFromIngoing();
+ var result2 = ingoing2.ToInternalFromIngoing();
+
+ // Assert
+ Assert.NotEqual(result1.Id, result2.Id);
+ }
+
+ [Fact]
+ public void ToInternalFromIngoing_WithZeroAges_MapsCorrectly()
+ {
+ // Arrange
+ var ingoing = new AltersGruppeIngoing
+ {
+ Name = "Beginners",
+ StartingAge = 0,
+ EndingAge = 0
+ };
+
+ // Act
+ var result = ingoing.ToInternalFromIngoing();
+
+ // Assert
+ Assert.Equal(0, result.StartingAge);
+ Assert.Equal(0, result.EndingAge);
+ }
+
+ [Fact]
+ public void ToInternalFromIngoing_WithNegativeAges_MapsCorrectly()
+ {
+ // Arrange - testing edge case, though business logic might not allow this
+ var ingoing = new AltersGruppeIngoing
+ {
+ Name = "Invalid",
+ StartingAge = -1,
+ EndingAge = -5
+ };
+
+ // Act
+ var result = ingoing.ToInternalFromIngoing();
+
+ // Assert
+ Assert.Equal(-1, result.StartingAge);
+ Assert.Equal(-5, result.EndingAge);
+ }
+
+ [Fact]
+ public void ToInternalFromIngoing_WithLargeAges_MapsCorrectly()
+ {
+ // Arrange
+ var ingoing = new AltersGruppeIngoing
+ {
+ Name = "Seniors",
+ StartingAge = 60,
+ EndingAge = 100
+ };
+
+ // Act
+ var result = ingoing.ToInternalFromIngoing();
+
+ // Assert
+ Assert.Equal(60, result.StartingAge);
+ Assert.Equal(100, result.EndingAge);
+ }
+}
diff --git a/API.Tests/Mappers/RegistrationKeyMapperTests.cs b/API.Tests/Mappers/RegistrationKeyMapperTests.cs
new file mode 100644
index 0000000..da0a915
--- /dev/null
+++ b/API.Tests/Mappers/RegistrationKeyMapperTests.cs
@@ -0,0 +1,123 @@
+using API.Models.Internal.User;
+
+namespace API.Tests.Mappers;
+
+public class RegistrationKeyMapperTests
+{
+ [Fact]
+ public void ToInternalFromIngoing_MapsLinkedRole()
+ {
+ // Arrange
+ var ingoing = new RegistrationKeyIngoing
+ {
+ LinkedRole = "Admin"
+ };
+
+ // Act
+ var result = ingoing.ToInternalFromIngoing();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("Admin", result.LinkedRole);
+ }
+
+ [Fact]
+ public void ToInternalFromIngoing_NullLinkedRole_MapsCorrectly()
+ {
+ // Arrange
+ var ingoing = new RegistrationKeyIngoing
+ {
+ LinkedRole = null
+ };
+
+ // Act
+ var result = ingoing.ToInternalFromIngoing();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Null(result.LinkedRole);
+ }
+
+ [Fact]
+ public void ToInternalFromIngoing_GeneratesNewId()
+ {
+ // Arrange
+ var ingoing = new RegistrationKeyIngoing
+ {
+ LinkedRole = "User"
+ };
+
+ // Act
+ var result = ingoing.ToInternalFromIngoing();
+
+ // Assert
+ Assert.NotNull(result.Id);
+ Assert.NotEmpty(result.Id);
+ }
+
+ [Fact]
+ public void ToInternalFromIngoing_GeneratesUniqueIds()
+ {
+ // Arrange
+ var ingoing1 = new RegistrationKeyIngoing { LinkedRole = "Admin" };
+ var ingoing2 = new RegistrationKeyIngoing { LinkedRole = "User" };
+
+ // Act
+ var result1 = ingoing1.ToInternalFromIngoing();
+ var result2 = ingoing2.ToInternalFromIngoing();
+
+ // Assert
+ Assert.NotEqual(result1.Id, result2.Id);
+ }
+
+ [Fact]
+ public void ToInternalFromIngoing_SetsCreatedDate()
+ {
+ // Arrange
+ var ingoing = new RegistrationKeyIngoing
+ {
+ LinkedRole = "Moderator"
+ };
+ var beforeCreation = DateTime.UtcNow;
+
+ // Act
+ var result = ingoing.ToInternalFromIngoing();
+
+ // Assert
+ var afterCreation = DateTime.UtcNow;
+ Assert.True(result.Created >= beforeCreation.AddSeconds(-1));
+ Assert.True(result.Created <= afterCreation.AddSeconds(1));
+ }
+
+ [Fact]
+ public void ToInternalFromIngoing_EmptyLinkedRole_MapsCorrectly()
+ {
+ // Arrange
+ var ingoing = new RegistrationKeyIngoing
+ {
+ LinkedRole = ""
+ };
+
+ // Act
+ var result = ingoing.ToInternalFromIngoing();
+
+ // Assert
+ Assert.Equal("", result.LinkedRole);
+ }
+
+ [Fact]
+ public void ToInternalFromIngoing_WhitespaceLinkedRole_MapsCorrectly()
+ {
+ // Arrange
+ var ingoing = new RegistrationKeyIngoing
+ {
+ LinkedRole = " "
+ };
+
+ // Act
+ var result = ingoing.ToInternalFromIngoing();
+
+ // Assert
+ Assert.Equal(" ", result.LinkedRole);
+ }
+}
diff --git a/API.Tests/Services/AgeGroupServiceTests.cs b/API.Tests/Services/AgeGroupServiceTests.cs
new file mode 100644
index 0000000..b0015dd
--- /dev/null
+++ b/API.Tests/Services/AgeGroupServiceTests.cs
@@ -0,0 +1,169 @@
+using API.Database;
+using API.Models.Internal.Altersgruppen;
+using API.Repository.AgeGroupRepo;
+using Microsoft.EntityFrameworkCore;
+
+namespace API.Tests.Services;
+
+public class AgeGroupServiceTests : IDisposable
+{
+ private readonly ApplicationDbContext _context;
+ private readonly AgeGroupService _service;
+
+ public AgeGroupServiceTests()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
+ .Options;
+
+ _context = new ApplicationDbContext(options);
+ _service = new AgeGroupService(_context);
+ }
+
+ public void Dispose()
+ {
+ _context.Database.EnsureDeleted();
+ _context.Dispose();
+ }
+
+ [Fact]
+ public async Task GetAllAsync_EmptyDatabase_ReturnsEmptyList()
+ {
+ // Act
+ var result = await _service.GetAllAsync();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public async Task GetAllAsync_WithData_ReturnsAllGroups()
+ {
+ // Arrange
+ var group1 = new AltersGruppe { Name = "U10", StartingAge = 8, EndingAge = 10 };
+ var group2 = new AltersGruppe { Name = "U12", StartingAge = 10, EndingAge = 12 };
+ await _context.Altersgruppen.AddRangeAsync(group1, group2);
+ await _context.SaveChangesAsync();
+
+ // Act
+ var result = await _service.GetAllAsync();
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Contains(result, g => g.Name == "U10");
+ Assert.Contains(result, g => g.Name == "U12");
+ }
+
+ [Fact]
+ public async Task GetAsync_ExistingId_ReturnsGroup()
+ {
+ // Arrange
+ var group = new AltersGruppe { Name = "U14", StartingAge = 12, EndingAge = 14 };
+ await _context.Altersgruppen.AddAsync(group);
+ await _context.SaveChangesAsync();
+
+ // Act
+ var result = await _service.GetAsync(group.Id);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("U14", result.Name);
+ Assert.Equal(12, result.StartingAge);
+ Assert.Equal(14, result.EndingAge);
+ }
+
+ [Fact]
+ public async Task GetAsync_NonExistingId_ReturnsNull()
+ {
+ // Act
+ var result = await _service.GetAsync("nonexistent-id");
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CreateAsync_ValidGroup_AddsToDatabase()
+ {
+ // Arrange
+ var newGroup = new AltersGruppe { Name = "U16", StartingAge = 14, EndingAge = 16 };
+
+ // Act
+ var result = await _service.CreateAsync(newGroup);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("U16", result.Name);
+
+ var dbGroup = await _context.Altersgruppen.FindAsync(result.Id);
+ Assert.NotNull(dbGroup);
+ Assert.Equal("U16", dbGroup.Name);
+ }
+
+ [Fact]
+ public async Task UpdateAsync_ExistingGroup_UpdatesProperties()
+ {
+ // Arrange
+ var group = new AltersGruppe { Name = "U18", StartingAge = 16, EndingAge = 18 };
+ await _context.Altersgruppen.AddAsync(group);
+ await _context.SaveChangesAsync();
+
+ var updatedGroup = new AltersGruppe { Name = "U18 Updated", StartingAge = 15, EndingAge = 18 };
+
+ // Act
+ var result = await _service.UpdateAsync(group.Id, updatedGroup);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("U18 Updated", result.Name);
+ Assert.Equal(15, result.StartingAge);
+ Assert.Equal(18, result.EndingAge);
+
+ var dbGroup = await _context.Altersgruppen.FindAsync(group.Id);
+ Assert.NotNull(dbGroup);
+ Assert.Equal("U18 Updated", dbGroup.Name);
+ }
+
+ [Fact]
+ public async Task UpdateAsync_NonExistingGroup_ReturnsNull()
+ {
+ // Arrange
+ var updatedGroup = new AltersGruppe { Name = "U18 Updated", StartingAge = 15, EndingAge = 18 };
+
+ // Act
+ var result = await _service.UpdateAsync("nonexistent-id", updatedGroup);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task DeleteAsync_ExistingGroup_RemovesFromDatabase()
+ {
+ // Arrange
+ var group = new AltersGruppe { Name = "U20", StartingAge = 18, EndingAge = 20 };
+ await _context.Altersgruppen.AddAsync(group);
+ await _context.SaveChangesAsync();
+
+ // Act
+ var result = await _service.DeleteAsync(group.Id);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("U20", result.Name);
+
+ var dbGroup = await _context.Altersgruppen.FindAsync(group.Id);
+ Assert.Null(dbGroup);
+ }
+
+ [Fact]
+ public async Task DeleteAsync_NonExistingGroup_ReturnsNull()
+ {
+ // Act
+ var result = await _service.DeleteAsync("nonexistent-id");
+
+ // Assert
+ Assert.Null(result);
+ }
+}
diff --git a/API.Tests/Services/PlunkEmailSenderTests.cs b/API.Tests/Services/PlunkEmailSenderTests.cs
new file mode 100644
index 0000000..02b478d
--- /dev/null
+++ b/API.Tests/Services/PlunkEmailSenderTests.cs
@@ -0,0 +1,214 @@
+using System.Net;
+using System.Text.Json;
+using API.Models.Outgoing;
+using API.Services;
+using Moq;
+using Moq.Protected;
+
+namespace API.Tests.Services;
+
+public class PlunkEmailSenderTests
+{
+ private const string TestSecretKey = "test-secret-key";
+ private const string PlunkApiUrl = "https://api.useplunk.com/v1/send";
+
+ private static HttpClient CreateMockHttpClient(HttpStatusCode statusCode, string responseContent = "{}")
+ {
+ var mockHandler = new Mock();
+
+ mockHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = statusCode,
+ Content = new StringContent(responseContent)
+ });
+
+ return new HttpClient(mockHandler.Object);
+ }
+
+ private static (HttpClient client, Mock handler) CreateMockHttpClientWithHandler(
+ HttpStatusCode statusCode, string responseContent = "{}")
+ {
+ var mockHandler = new Mock();
+
+ mockHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = statusCode,
+ Content = new StringContent(responseContent)
+ });
+
+ return (new HttpClient(mockHandler.Object), mockHandler);
+ }
+
+ [Fact]
+ public async Task SendEmailAsync_SuccessfulSend_DoesNotThrow()
+ {
+ // Arrange
+ var httpClient = CreateMockHttpClient(HttpStatusCode.OK);
+ var emailSender = new PlunkEmailSender(httpClient, TestSecretKey);
+
+ // Act & Assert - should not throw
+ await emailSender.SendEmailAsync("test@example.com", "Test Subject", "Test Body
");
+ }
+
+ [Fact]
+ public async Task SendEmailAsync_ApiReturnsError_ThrowsException()
+ {
+ // Arrange
+ var errorMessage = "Invalid API key";
+ var httpClient = CreateMockHttpClient(HttpStatusCode.Unauthorized, errorMessage);
+ var emailSender = new PlunkEmailSender(httpClient, TestSecretKey);
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(() =>
+ emailSender.SendEmailAsync("test@example.com", "Test Subject", "Test Body
"));
+
+ Assert.Contains("Fehler beim Senden der E-Mail über Plunk", exception.Message);
+ Assert.Contains(errorMessage, exception.Message);
+ }
+
+ [Fact]
+ public async Task SendEmailAsync_ServerError_ThrowsException()
+ {
+ // Arrange
+ var httpClient = CreateMockHttpClient(HttpStatusCode.InternalServerError, "Server error");
+ var emailSender = new PlunkEmailSender(httpClient, TestSecretKey);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(() =>
+ emailSender.SendEmailAsync("test@example.com", "Test Subject", "Test Body
"));
+ }
+
+ [Fact]
+ public async Task SendEmailAsync_BadRequest_ThrowsException()
+ {
+ // Arrange
+ var httpClient = CreateMockHttpClient(HttpStatusCode.BadRequest, "Invalid request");
+ var emailSender = new PlunkEmailSender(httpClient, TestSecretKey);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(() =>
+ emailSender.SendEmailAsync("test@example.com", "Test Subject", "Test Body
"));
+ }
+
+ [Fact]
+ public async Task SendEmailAsync_SendsCorrectPayload()
+ {
+ // Arrange
+ var mockHandler = new Mock();
+ HttpRequestMessage? capturedRequest = null;
+
+ mockHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .Callback((request, _) => capturedRequest = request)
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent("{}")
+ });
+
+ var httpClient = new HttpClient(mockHandler.Object);
+ var emailSender = new PlunkEmailSender(httpClient, TestSecretKey);
+
+ // Act
+ await emailSender.SendEmailAsync("recipient@example.com", "My Subject", "Hello
");
+
+ // Assert
+ Assert.NotNull(capturedRequest);
+ Assert.Equal(HttpMethod.Post, capturedRequest.Method);
+ Assert.Equal(PlunkApiUrl, capturedRequest.RequestUri?.ToString());
+
+ var requestContent = await capturedRequest.Content!.ReadAsStringAsync();
+ var payload = JsonSerializer.Deserialize(requestContent, new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ });
+
+ Assert.NotNull(payload);
+ Assert.Equal("recipient@example.com", payload.To);
+ Assert.Equal("My Subject", payload.Subject);
+ Assert.Equal("Hello
", payload.Body);
+ }
+
+ [Fact]
+ public async Task SendEmailAsync_SendsCorrectAuthorizationHeader()
+ {
+ // Arrange
+ var mockHandler = new Mock();
+ HttpRequestMessage? capturedRequest = null;
+
+ mockHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .Callback((request, _) => capturedRequest = request)
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent("{}")
+ });
+
+ var httpClient = new HttpClient(mockHandler.Object);
+ var emailSender = new PlunkEmailSender(httpClient, "my-secret-api-key");
+
+ // Act
+ await emailSender.SendEmailAsync("test@example.com", "Subject", "Body");
+
+ // Assert
+ Assert.NotNull(capturedRequest);
+ var authHeader = httpClient.DefaultRequestHeaders.Authorization;
+ Assert.NotNull(authHeader);
+ Assert.Equal("Bearer", authHeader.Scheme);
+ Assert.Equal("my-secret-api-key", authHeader.Parameter);
+ }
+
+ [Fact]
+ public async Task SendEmailAsync_SendsCorrectContentType()
+ {
+ // Arrange
+ var mockHandler = new Mock();
+ HttpRequestMessage? capturedRequest = null;
+
+ mockHandler
+ .Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .Callback((request, _) => capturedRequest = request)
+ .ReturnsAsync(new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent("{}")
+ });
+
+ var httpClient = new HttpClient(mockHandler.Object);
+ var emailSender = new PlunkEmailSender(httpClient, TestSecretKey);
+
+ // Act
+ await emailSender.SendEmailAsync("test@example.com", "Subject", "Body");
+
+ // Assert
+ Assert.NotNull(capturedRequest);
+ Assert.NotNull(capturedRequest.Content);
+ Assert.Equal("application/json", capturedRequest.Content.Headers.ContentType?.MediaType);
+ Assert.Equal("utf-8", capturedRequest.Content.Headers.ContentType?.CharSet);
+ }
+}
diff --git a/API.Tests/Services/RegistrationKeyServiceTests.cs b/API.Tests/Services/RegistrationKeyServiceTests.cs
new file mode 100644
index 0000000..ec9fd2e
--- /dev/null
+++ b/API.Tests/Services/RegistrationKeyServiceTests.cs
@@ -0,0 +1,244 @@
+using API.Database;
+using API.Models.Internal.User;
+using API.Repository.RegistrationKeyRepo;
+using Microsoft.EntityFrameworkCore;
+
+namespace API.Tests.Services;
+
+public class RegistrationKeyServiceTests : IDisposable
+{
+ private readonly ApplicationDbContext _context;
+ private readonly RegistrationKeyService _service;
+
+ public RegistrationKeyServiceTests()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
+ .Options;
+
+ _context = new ApplicationDbContext(options);
+ _service = new RegistrationKeyService(_context);
+ }
+
+ public void Dispose()
+ {
+ _context.Database.EnsureDeleted();
+ _context.Dispose();
+ }
+
+ [Fact]
+ public async Task GetAllAsync_EmptyDatabase_ReturnsEmptyList()
+ {
+ // Act
+ var result = await _service.GetAllAsync();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public async Task GetAllAsync_WithData_ReturnsAllKeys()
+ {
+ // Arrange
+ var key1 = new RegistrationKey { LinkedRole = "Admin" };
+ var key2 = new RegistrationKey { LinkedRole = "User" };
+ await _context.RegistrationKeys.AddRangeAsync(key1, key2);
+ await _context.SaveChangesAsync();
+
+ // Act
+ var result = await _service.GetAllAsync();
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Contains(result, k => k.LinkedRole == "Admin");
+ Assert.Contains(result, k => k.LinkedRole == "User");
+ }
+
+ [Fact]
+ public async Task GetAsync_ExistingId_ReturnsKey()
+ {
+ // Arrange
+ var key = new RegistrationKey { LinkedRole = "Moderator" };
+ await _context.RegistrationKeys.AddAsync(key);
+ await _context.SaveChangesAsync();
+
+ // Act
+ var result = await _service.GetAsync(key.Id);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("Moderator", result.LinkedRole);
+ }
+
+ [Fact]
+ public async Task GetAsync_NonExistingId_ReturnsNull()
+ {
+ // Act
+ var result = await _service.GetAsync("nonexistent-id");
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task CreateAsync_ValidKey_AddsToDatabase()
+ {
+ // Arrange
+ var keyIngoing = new RegistrationKeyIngoing { LinkedRole = "Admin" };
+
+ // Act
+ var result = await _service.CreateAsync(keyIngoing);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("Admin", result.LinkedRole);
+ Assert.NotNull(result.Id);
+
+ var dbKey = await _context.RegistrationKeys.FindAsync(result.Id);
+ Assert.NotNull(dbKey);
+ Assert.Equal("Admin", dbKey.LinkedRole);
+ }
+
+ [Fact]
+ public async Task CreateAsync_NullRole_AddsToDatabase()
+ {
+ // Arrange
+ var keyIngoing = new RegistrationKeyIngoing { LinkedRole = null };
+
+ // Act
+ var result = await _service.CreateAsync(keyIngoing);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Null(result.LinkedRole);
+
+ var dbKey = await _context.RegistrationKeys.FindAsync(result.Id);
+ Assert.NotNull(dbKey);
+ }
+
+ [Fact]
+ public async Task DeleteAsync_ExistingKey_RemovesFromDatabase()
+ {
+ // Arrange
+ var key = new RegistrationKey { LinkedRole = "ToDelete" };
+ await _context.RegistrationKeys.AddAsync(key);
+ await _context.SaveChangesAsync();
+
+ // Act
+ var result = await _service.DeleteAsync(key.Id);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("ToDelete", result.LinkedRole);
+
+ var dbKey = await _context.RegistrationKeys.FindAsync(key.Id);
+ Assert.Null(dbKey);
+ }
+
+ [Fact]
+ public async Task DeleteAsync_NonExistingKey_ReturnsNull()
+ {
+ // Act
+ var result = await _service.DeleteAsync("nonexistent-id");
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task DeleteOldRegistrationKeysAsync_ZeroDays_ReturnsZero()
+ {
+ // Arrange
+ var key = new RegistrationKey { LinkedRole = "Old", Created = DateTime.UtcNow.AddDays(-10) };
+ await _context.RegistrationKeys.AddAsync(key);
+ await _context.SaveChangesAsync();
+
+ // Act
+ var result = await _service.DeleteOldRegistrationKeysAsync(0);
+
+ // Assert
+ Assert.Equal(0, result);
+ Assert.Single(await _context.RegistrationKeys.ToListAsync());
+ }
+
+ [Fact]
+ public async Task DeleteOldRegistrationKeysAsync_NegativeDays_ReturnsZero()
+ {
+ // Arrange
+ var key = new RegistrationKey { LinkedRole = "Old", Created = DateTime.UtcNow.AddDays(-10) };
+ await _context.RegistrationKeys.AddAsync(key);
+ await _context.SaveChangesAsync();
+
+ // Act
+ var result = await _service.DeleteOldRegistrationKeysAsync(-5);
+
+ // Assert
+ Assert.Equal(0, result);
+ Assert.Single(await _context.RegistrationKeys.ToListAsync());
+ }
+
+ [Fact]
+ public async Task DeleteOldRegistrationKeysAsync_NoOldKeys_ReturnsZero()
+ {
+ // Arrange
+ var key = new RegistrationKey { LinkedRole = "New", Created = DateTime.UtcNow };
+ await _context.RegistrationKeys.AddAsync(key);
+ await _context.SaveChangesAsync();
+
+ // Act
+ var result = await _service.DeleteOldRegistrationKeysAsync(7);
+
+ // Assert
+ Assert.Equal(0, result);
+ Assert.Single(await _context.RegistrationKeys.ToListAsync());
+ }
+
+ [Fact]
+ public async Task DeleteOldRegistrationKeysAsync_WithOldKeys_DeletesOldKeysOnly()
+ {
+ // Arrange
+ var oldKey1 = new RegistrationKey { LinkedRole = "Old1", Created = DateTime.UtcNow.AddDays(-10) };
+ var oldKey2 = new RegistrationKey { LinkedRole = "Old2", Created = DateTime.UtcNow.AddDays(-15) };
+ var newKey = new RegistrationKey { LinkedRole = "New", Created = DateTime.UtcNow.AddDays(-2) };
+
+ await _context.RegistrationKeys.AddRangeAsync(oldKey1, oldKey2, newKey);
+ await _context.SaveChangesAsync();
+
+ // Act
+ var result = await _service.DeleteOldRegistrationKeysAsync(7);
+
+ // Assert
+ Assert.Equal(2, result);
+
+ var remainingKeys = await _context.RegistrationKeys.ToListAsync();
+ Assert.Single(remainingKeys);
+ Assert.Equal("New", remainingKeys[0].LinkedRole);
+ }
+
+ [Fact]
+ public async Task DeleteOldRegistrationKeysAsync_ExactCutoffDate_KeyNotDeleted()
+ {
+ // Arrange - key created exactly at cutoff should NOT be deleted
+ var keyAtCutoff = new RegistrationKey { LinkedRole = "AtCutoff", Created = DateTime.UtcNow.AddDays(-7) };
+ await _context.RegistrationKeys.AddAsync(keyAtCutoff);
+ await _context.SaveChangesAsync();
+
+ // Act
+ var result = await _service.DeleteOldRegistrationKeysAsync(7);
+
+ // Assert - cutoff is < not <=, so exactly 7 days old should not be deleted
+ Assert.Equal(0, result);
+ Assert.Single(await _context.RegistrationKeys.ToListAsync());
+ }
+
+ [Fact]
+ public async Task DeleteOldRegistrationKeysAsync_EmptyDatabase_ReturnsZero()
+ {
+ // Act
+ var result = await _service.DeleteOldRegistrationKeysAsync(7);
+
+ // Assert
+ Assert.Equal(0, result);
+ }
+}
diff --git a/JudoWeb.sln b/JudoWeb.sln
index 0cbf2db..4140b6d 100644
--- a/JudoWeb.sln
+++ b/JudoWeb.sln
@@ -1,10 +1,12 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36518.9 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "API\API.csproj", "{98166726-DC3A-4D5B-889A-8B4428E28656}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API.Tests", "API.Tests\API.Tests.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
{98166726-DC3A-4D5B-889A-8B4428E28656}.Debug|Any CPU.Build.0 = Debug|Any CPU
{98166726-DC3A-4D5B-889A-8B4428E28656}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98166726-DC3A-4D5B-889A-8B4428E28656}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE