Merge pull request #4 from Govor-team/master

test merge
This commit is contained in:
Artemy
2026-02-08 22:16:11 +07:00
committed by GitHub
344 changed files with 19230 additions and 3455 deletions
+11 -8
View File
@@ -10,21 +10,24 @@ on:
branches: [ "master" ] branches: [ "master" ]
jobs: jobs:
build: build-and-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - name: Checkout code
- name: Setup .NET uses: actions/checkout@v4
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: 9.0.x dotnet-version: '8.0.x'
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Build - name: Build
run: dotnet build --no-restore --configuration Release run: dotnet build --no-restore --configuration Release
- name: Test API
run: dotnet test ./Govor.API.Tests/Govor.API.Tests.csproj - name: Run tests
run: dotnet test Govor.API.Tests/Govor.API.Tests.csproj
+22
View File
@@ -0,0 +1,22 @@
name: Qodana Analysis
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
qodana:
name: Run Qodana Code Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run Qodana
uses: JetBrains/qodana-action@v2024.1
with:
# Указываем путь к qodana.yaml (если он не в корне)
args: --ide QDNET --project-dir .
+26
View File
@@ -0,0 +1,26 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Êîïèðóåì òîëüêî .sln è .csproj
COPY *.sln ./
COPY Govor.API/*.csproj ./Govor.API/
COPY Govor.Application/*.csproj ./Govor.Application/
COPY Govor.Core/*.csproj ./Govor.Core/
COPY Govor.Data/*.csproj ./Govor.Data/
COPY Govor.Contracts/*.csproj ./Govor.Contracts/
RUN dotnet restore Govor.API/Govor.API.csproj
# Êîïèðóåì âåñü êîä
COPY . .
WORKDIR /src/Govor.API
RUN dotnet publish -c Release -o /app/publish
# Runtime image
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "Govor.API.dll"]
@@ -0,0 +1,114 @@
using System.Security.Claims;
using Govor.API.Common.SignalR.Helpers;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.API.Tests.Common.SignalR.Helpers;
[TestFixture]
[TestOf(typeof(HubUserAccessor))]
public class HubUserAccessorTests
{
private HubUserAccessor _accessor;
private Mock<ILogger<HubUserAccessor>> _loggerMock;
[SetUp]
public void SetUp()
{
_loggerMock = new Mock<ILogger<HubUserAccessor>>();
_accessor = new HubUserAccessor(_loggerMock.Object);
}
private HubCallerContext CreateContextWithClaims(params Claim[] claims)
{
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims));
var contextMock = new Mock<HubCallerContext>();
contextMock.Setup(c => c.User).Returns(principal);
return contextMock.Object;
}
[Test]
public void GetUserId_ValidClaim_ReturnsGuid()
{
// Arrange
var expectedGuid = Guid.NewGuid();
var context = CreateContextWithClaims(new Claim("userId", expectedGuid.ToString()));
// Act
var result = _accessor.GetUserId(context);
// Assert
Assert.That(expectedGuid, Is.EqualTo(result));
}
[Test]
public void GetUserId_InvalidClaim_ThrowsException_WhenNotSuppressed()
{
// Arrange
var context = CreateContextWithClaims(new Claim("userId", "not-a-guid"));
// Act & Assert
Assert.Throws<UnauthorizedAccessException>(() => _accessor.GetUserId(context, suppressException: false));
}
[Test]
public void GetUserId_InvalidClaim_ReturnsEmptyGuid_WhenSuppressed()
{
// Arrange
var context = CreateContextWithClaims(new Claim("userId", "not-a-guid"));
// Act
var result = _accessor.GetUserId(context, suppressException: true);
// Assert
Assert.That(Guid.Empty, Is.EqualTo(result));
}
[Test]
public void GetUserId_NoClaim_ThrowsException_WhenNotSuppressed()
{
// Arrange
var context = CreateContextWithClaims();
// Act & Assert
Assert.Throws<UnauthorizedAccessException>(() => _accessor.GetUserId(context, suppressException: false));
}
[Test]
public void GetUserId_NoClaim_ReturnsEmptyGuid_WhenSuppressed()
{
// Arrange
var context = CreateContextWithClaims();
// Act
var result = _accessor.GetUserId(context, suppressException: true);
// Assert
Assert.That(Guid.Empty, Is.EqualTo(result));
}
[Test]
public void GetUserId_UserIsNull_ReturnsEmptyGuid_WhenSuppressed()
{
// Arrange
var contextMock = new Mock<HubCallerContext>();
contextMock.Setup(c => c.User).Returns((ClaimsPrincipal?)null);
// Act
var result = _accessor.GetUserId(contextMock.Object, suppressException: true);
// Assert
Assert.That(Guid.Empty, Is.EqualTo(result));
}
[Test]
public void GetUserId_UserIsNull_ThrowsException_WhenNotSuppressed()
{
// Arrange
var contextMock = new Mock<HubCallerContext>();
contextMock.Setup(c => c.User).Returns((ClaimsPrincipal?)null);
// Act & Assert
Assert.Throws<UnauthorizedAccessException>(() => _accessor.GetUserId(contextMock.Object, suppressException: false));
}
}
@@ -0,0 +1,389 @@
using AutoMapper;
using Govor.API.Controllers;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Application.Interfaces.UserSession;
using Govor.Contracts.DTOs;
using Govor.Core.Models.Users;
using Govor.Data.Repositories.Exceptions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.API.Tests.Controllers;
[TestFixture]
[TestOf(typeof(SessionController))]
public class SessionControllerTests
{
private Mock<ILogger<SessionController>> _loggerMock;
private Mock<ICurrentUserService> _currentUserServiceMock;
private Mock<ICurrentUserSessionService> _currentUserSessionServiceMock;
private Mock<IUserSessionReader> _userSessionReaderMock;
private Mock<IUserSessionRevoker> _userSessionRevokerMock;
private Mock<IMapper> _mapperMock;
private SessionController _controller;
[SetUp]
public void SetUp()
{
_loggerMock = new Mock<ILogger<SessionController>>();
_currentUserServiceMock = new Mock<ICurrentUserService>();
_currentUserSessionServiceMock = new Mock<ICurrentUserSessionService>();
_userSessionReaderMock = new Mock<IUserSessionReader>();
_userSessionRevokerMock = new Mock<IUserSessionRevoker>();
_mapperMock = new Mock<IMapper>();
_controller = new SessionController(
_loggerMock.Object,
_currentUserServiceMock.Object,
_currentUserSessionServiceMock.Object,
_userSessionReaderMock.Object,
_userSessionRevokerMock.Object,
_mapperMock.Object);
}
#region GetAllSessions
[Test]
public async Task GetAllSessions_Successful_ReturnsOkWithMappedSessions()
{
// Arrange
var userId = Guid.NewGuid();
var sessions = new List<UserSession> { new UserSession { Id = Guid.NewGuid(), UserId = userId } };
var sessionDtos = new List<SessionDto> { new SessionDto { Id = sessions[0].Id } };
_currentUserServiceMock.Setup(s => s.GetCurrentUserId()).Returns(userId);
_userSessionReaderMock.Setup(r => r.GetAllSessionsAsync(userId)).ReturnsAsync(sessions);
_mapperMock.Setup(m => m.Map<List<SessionDto>>(sessions)).Returns(sessionDtos);
// Act
var result = await _controller.GetAllSessions();
// Assert
Assert.That(result, Is.TypeOf<OkObjectResult>());
var okResult = result as OkObjectResult;
Assert.That(okResult.Value, Is.EqualTo(sessionDtos));
_loggerMock.VerifyNoOtherCalls();
}
[Test]
public async Task GetAllSessions_UnauthorizedAccess_ReturnsForbid()
{
// Arrange
var userId = Guid.NewGuid();
var exception = new UnauthorizedAccessException("Unauthorized");
_currentUserServiceMock.Setup(s => s.GetCurrentUserId()).Returns(userId);
_userSessionReaderMock.Setup(r => r.GetAllSessionsAsync(userId)).ThrowsAsync(exception);
// Act
var result = await _controller.GetAllSessions();
// Assert
Assert.That(result, Is.TypeOf<ForbidResult>());
_loggerMock.Verify(l => l.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
exception,
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once());
}
[Test]
public async Task GetAllSessions_UnexpectedError_Returns500()
{
// Arrange
var userId = Guid.NewGuid();
var exception = new Exception("Unexpected error");
_currentUserServiceMock.Setup(s => s.GetCurrentUserId()).Returns(userId);
_userSessionReaderMock.Setup(r => r.GetAllSessionsAsync(userId)).ThrowsAsync(exception);
// Act
var result = await _controller.GetAllSessions();
// Assert
Assert.That(result, Is.TypeOf<ObjectResult>());
var statusResult = result as ObjectResult;
Assert.That(statusResult.StatusCode, Is.EqualTo(500));
Assert.That(statusResult.Value, Is.EqualTo("Unexpected Error! Please try again later."));
_loggerMock.Verify(l => l.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
exception,
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once());
}
#endregion
#region CloseSession
[Test]
public async Task CloseSession_ValidSessionId_ReturnsOk()
{
// Arrange
var userId = Guid.NewGuid();
var sessionId = Guid.NewGuid();
_currentUserServiceMock.Setup(s => s.GetCurrentUserId()).Returns(userId);
// Act
var result = await _controller.CloseSession(sessionId);
// Assert
Assert.That(result, Is.TypeOf<OkResult>());
_userSessionRevokerMock.Verify(r => r.CloseSessionByIdAsync(sessionId, userId), Times.Once());
_loggerMock.VerifyNoOtherCalls();
}
[Test]
public async Task CloseSession_EmptySessionId_ReturnsBadRequest()
{
// Arrange
var sessionId = Guid.Empty;
// Act
var result = await _controller.CloseSession(sessionId);
// Assert
Assert.That(result, Is.TypeOf<BadRequestObjectResult>());
var badRequestResult = result as BadRequestObjectResult;
Assert.That(badRequestResult.Value, Is.EqualTo("Invalid sessionId."));
_userSessionRevokerMock.Verify(r => r.CloseSessionByIdAsync(It.IsAny<Guid>(), It.IsAny<Guid>()), Times.Never());
_loggerMock.VerifyNoOtherCalls();
}
[Test]
public async Task CloseSession_UnauthorizedAccess_ReturnsForbid()
{
// Arrange
var userId = Guid.NewGuid();
var sessionId = Guid.NewGuid();
var exception = new UnauthorizedAccessException("Unauthorized");
_currentUserServiceMock.Setup(s => s.GetCurrentUserId()).Returns(userId);
_userSessionRevokerMock.Setup(r => r.CloseSessionByIdAsync(sessionId, userId)).ThrowsAsync(exception);
// Act
var result = await _controller.CloseSession(sessionId);
// Assert
Assert.That(result, Is.TypeOf<ForbidResult>());
_loggerMock.Verify(l => l.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
exception,
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once());
}
[Test]
public async Task CloseSession_NotFound_ReturnsNotFound()
{
// Arrange
var userId = Guid.NewGuid();
var sessionId = Guid.NewGuid();
var exception = new NotFoundException("Session not found");
_currentUserServiceMock.Setup(s => s.GetCurrentUserId()).Returns(userId);
_userSessionRevokerMock.Setup(r => r.CloseSessionByIdAsync(sessionId, userId)).ThrowsAsync(exception);
// Act
var result = await _controller.CloseSession(sessionId);
// Assert
Assert.That(result, Is.TypeOf<NotFoundObjectResult>());
var notFoundResult = result as NotFoundObjectResult;
Assert.That(notFoundResult.Value, Is.EqualTo("Session not found"));
_loggerMock.Verify(l => l.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
exception,
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once());
}
[Test]
public async Task CloseSession_InvalidOperation_ReturnsBadRequest()
{
// Arrange
var userId = Guid.NewGuid();
var sessionId = Guid.NewGuid();
var exception = new InvalidOperationException("Invalid operation");
_currentUserServiceMock.Setup(s => s.GetCurrentUserId()).Returns(userId);
_userSessionRevokerMock.Setup(r => r.CloseSessionByIdAsync(sessionId, userId)).ThrowsAsync(exception);
// Act
var result = await _controller.CloseSession(sessionId);
// Assert
Assert.That(result, Is.TypeOf<BadRequestObjectResult>());
var badRequestResult = result as BadRequestObjectResult;
Assert.That(badRequestResult.Value, Is.EqualTo("Invalid operation"));
_loggerMock.Verify(l => l.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
exception,
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once());
}
#endregion
#region CloseCurrentSession
[Test]
public async Task CloseCurrentSession_ValidSessionIdAndUserId_ReturnsOk()
{
// Arrange
var userId = Guid.NewGuid();
var sessionId = Guid.NewGuid();
_currentUserServiceMock.Setup(s => s.GetCurrentUserId()).Returns(userId);
_currentUserSessionServiceMock.Setup(s => s.GetUserSessionId()).Returns(sessionId);
// Act
var result = await _controller.CloseCurrent();
// Assert
Assert.That(result, Is.TypeOf<OkResult>());
_userSessionRevokerMock.Verify(r => r.CloseSessionByIdAsync(sessionId, userId), Times.Once());
_loggerMock.VerifyNoOtherCalls();
}
[Test]
public async Task CloseCurrentSession_UnauthorizedAccess_ReturnsForbid()
{
// Arrange
var userId = Guid.NewGuid();
var sessionId = Guid.NewGuid();
var exception = new UnauthorizedAccessException("Unauthorized");
_currentUserServiceMock.Setup(s => s.GetCurrentUserId()).Returns(userId);
_currentUserSessionServiceMock.Setup(s => s.GetUserSessionId()).Returns(sessionId);
_userSessionRevokerMock.Setup(r => r.CloseSessionByIdAsync(sessionId, userId)).ThrowsAsync(exception);
// Act
var result = await _controller.CloseCurrent();
// Assert
Assert.That(result, Is.TypeOf<ForbidResult>());
_loggerMock.Verify(l => l.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
exception,
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once());
}
[Test]
public async Task CloseCurrentSession_NotFound_ReturnsNotFound()
{
// Arrange
var userId = Guid.NewGuid();
var sessionId = Guid.NewGuid();
var exception = new NotFoundException("Session not found");
_currentUserServiceMock.Setup(s => s.GetCurrentUserId()).Returns(userId);
_currentUserSessionServiceMock.Setup(s => s.GetUserSessionId()).Returns(sessionId);
_userSessionRevokerMock.Setup(r => r.CloseSessionByIdAsync(sessionId, userId)).ThrowsAsync(exception);
// Act
var result = await _controller.CloseCurrent();
// Assert
Assert.That(result, Is.TypeOf<NotFoundObjectResult>());
var notFoundResult = result as NotFoundObjectResult;
Assert.That(notFoundResult.Value, Is.EqualTo("Session not found"));
_loggerMock.Verify(l => l.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
exception,
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once());
}
[Test]
public async Task CloseCurrentSession_InvalidOperation_ReturnsBadRequest()
{
// Arrange
var userId = Guid.NewGuid();
var sessionId = Guid.NewGuid();
var exception = new InvalidOperationException("Invalid operation");
_currentUserServiceMock.Setup(s => s.GetCurrentUserId()).Returns(userId);
_currentUserSessionServiceMock.Setup(s => s.GetUserSessionId()).Returns(sessionId);
_userSessionRevokerMock.Setup(r => r.CloseSessionByIdAsync(sessionId, userId)).ThrowsAsync(exception);
// Act
var result = await _controller.CloseCurrent();
// Assert
Assert.That(result, Is.TypeOf<BadRequestObjectResult>());
var badRequestResult = result as BadRequestObjectResult;
Assert.That(badRequestResult.Value, Is.EqualTo("Invalid operation"));
_loggerMock.Verify(l => l.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
exception,
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once());
}
#endregion
#region CloseAllSessions
[Test]
public async Task CloseAllSessions_Successful_ReturnsOk()
{
// Arrange
var userId = Guid.NewGuid();
_currentUserServiceMock.Setup(s => s.GetCurrentUserId()).Returns(userId);
// Act
var result = await _controller.CloseAllSessions();
// Assert
Assert.That(result, Is.TypeOf<OkResult>());
_userSessionRevokerMock.Verify(r => r.CloseAllSessionsAsync(userId), Times.Once());
_loggerMock.VerifyNoOtherCalls();
}
[Test]
public async Task CloseAllSessions_UnauthorizedAccess_ReturnsForbid()
{
// Arrange
var userId = Guid.NewGuid();
var exception = new UnauthorizedAccessException("Unauthorized");
_currentUserServiceMock.Setup(s => s.GetCurrentUserId()).Returns(userId);
_userSessionRevokerMock.Setup(r => r.CloseAllSessionsAsync(userId)).ThrowsAsync(exception);
// Act
var result = await _controller.CloseAllSessions();
// Assert
Assert.That(result, Is.TypeOf<ForbidResult>());
_loggerMock.Verify(l => l.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
exception,
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once());
}
[Test]
public async Task CloseAllSessions_UnexpectedError_Returns500()
{
// Arrange
var userId = Guid.NewGuid();
var exception = new Exception("Unexpected error");
_currentUserServiceMock.Setup(s => s.GetCurrentUserId()).Returns(userId);
_userSessionRevokerMock.Setup(r => r.CloseAllSessionsAsync(userId)).ThrowsAsync(exception);
// Act
var result = await _controller.CloseAllSessions();
// Assert
Assert.That(result, Is.TypeOf<ObjectResult>());
var statusResult = result as ObjectResult;
Assert.That(statusResult.StatusCode, Is.EqualTo(500));
Assert.That(statusResult.Value, Is.EqualTo("Unexpected Error! Please try again later."));
_loggerMock.Verify(l => l.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
exception,
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once());
}
#endregion
[TearDown]
public void TearDown()
{
_controller?.Dispose();
}
}
+7 -7
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@@ -11,13 +11,14 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoFixture" Version="5.0.0-preview0012" /> <PackageReference Include="AutoFixture" Version="5.0.0-preview0012" />
<PackageReference Include="coverlet.collector" Version="6.0.2" /> <PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Moq" Version="4.20.72" /> <PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.2.2" /> <PackageReference Include="NUnit" Version="4.4.0-beta.1" />
<PackageReference Include="NUnit.Analyzers" Version="4.3.0" /> <PackageReference Include="NUnit.Analyzers" Version="4.4.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.6.0"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -28,5 +29,4 @@
<ProjectReference Include="..\Govor.API\Govor.API.csproj" /> <ProjectReference Include="..\Govor.API\Govor.API.csproj" />
<ProjectReference Include="..\Govor.Data\Govor.Data.csproj" /> <ProjectReference Include="..\Govor.Data\Govor.Data.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+196
View File
@@ -0,0 +1,196 @@
using AutoFixture;
using Govor.API.Common.SignalR.Helpers;
using Govor.API.Hubs;
using Govor.Application.Interfaces.UserOnlineStatus;
using Govor.Core.Models.Users;
using Govor.Core.Repositories.Users;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.API.Tests.Hubs;
[TestFixture]
[TestOf(typeof(PresenceHub))]
public class PresenceHubTests
{
private Mock<ILogger<PresenceHub>> _mockLogger;
private Mock<IUserNotificationScopeService> _mockUserNotification;
private Mock<IOnlineUserStore> _mockOnlineUserStore;
private Mock<IHubUserAccessor> _mockHubUserAccessor;
private Mock<IUsersRepository> _mockUserRepository;
private Mock<IHubCallerClients> _clientsMock = null!;
private Mock<IClientProxy> _clientProxyMock = null!;
private Mock<HubCallerContext> _mockContext = null!;
private Mock<IGroupManager> _groupManagerMock = null!;
private Fixture _fixture;
private PresenceHub _hub;
private Guid _userId;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_mockLogger = new Mock<ILogger<PresenceHub>>();
_mockUserNotification = new Mock<IUserNotificationScopeService>();
_mockOnlineUserStore = new Mock<IOnlineUserStore>();
_mockHubUserAccessor = new Mock<IHubUserAccessor>();
_mockUserRepository = new Mock<IUsersRepository>();
_clientsMock = new Mock<IHubCallerClients>();
_clientProxyMock = new Mock<IClientProxy>();
_userId = Guid.NewGuid();
_mockHubUserAccessor.Setup(h => h.GetUserId(
It.IsAny<HubCallerContext>(),
It.IsAny<bool>()))
.Returns(_userId);
_clientsMock.Setup(c => c.Group(It.IsAny<string>())).Returns(_clientProxyMock.Object);
_mockContext = new Mock<HubCallerContext>();
_mockContext.Setup(c => c.ConnectionId).Returns("test-connection");
_groupManagerMock = new Mock<IGroupManager>();
_groupManagerMock.Setup(g => g.AddToGroupAsync(It.IsAny<string>(), It.IsAny<string>(), default))
.Returns(Task.CompletedTask);
_hub = new PresenceHub(
_mockLogger.Object,
_mockUserNotification.Object,
_mockOnlineUserStore.Object,
_mockHubUserAccessor.Object,
_mockUserRepository.Object)
{
Clients = _clientsMock.Object,
Context = _mockContext.Object,
Groups = _groupManagerMock.Object,
};
}
// Tests for OnConnectedAsync action
[Test]
public async Task OnConnectedAsync_SetsUserOnlineAndNotifiesFriends()
{
// Arrange
_mockUserRepository.Setup(f => f.ExistsByIdAsync(_userId))
.ReturnsAsync(true);
var friendsIds = _fixture.CreateMany<Guid>().ToList();
_mockUserNotification.Setup(n => n.GetNotifiedUsers(_userId))
.ReturnsAsync(friendsIds);
// Act
await _hub.OnConnectedAsync();
// Assert
_mockOnlineUserStore.Verify(store => store.SetOnlineUser(_userId), Times.Once);
_clientsMock.Verify(c => c.Group(It.IsAny<string>()), Times.Exactly(friendsIds.Count));
foreach (var friendId in friendsIds)
{
_clientProxyMock.Verify(proxy =>
proxy.SendCoreAsync("UserOnline",
It.Is<object[]>(args => args.Length == 1 && (Guid)args[0] == _userId),
It.IsAny<CancellationToken>()),
Times.Exactly(friendsIds.Count));
}
}
[Test]
public async Task OnConnectedAsync_UserIdInvalidOrNotExists_AbortsConnection()
{
// Arrange
_mockHubUserAccessor.Setup(h => h.GetUserId(It.IsAny<HubCallerContext>(), false))
.Returns(Guid.Empty);
// Act
await _hub.OnConnectedAsync();
// Assert
_mockOnlineUserStore.Verify(store => store.SetOnlineUser(It.IsAny<Guid>()), Times.Never);
_mockContext.Verify(c => c.Abort(), Times.Once);
}
[Test]
public async Task OnConnectedAsync_UserNotExists_AbortsConnection()
{
// Arrange
_mockUserRepository.Setup(f => f.ExistsByIdAsync(_userId))
.ReturnsAsync(false);
// Act
await _hub.OnConnectedAsync();
// Assert
_mockOnlineUserStore.Verify(store => store.SetOnlineUser(It.IsAny<Guid>()), Times.Never);
_mockContext.Verify(c => c.Abort(), Times.Once);
}
// Tests for OnDisconnectedAsync action
[Test]
public async Task OnDisconnectedAsync_SetsUserOffline_UpdatesWasOnline_NotifiesFriends()
{
// Arrange
var user = new User { Id = _userId, WasOnline = DateTime.MinValue };
var friendsIds = _fixture.CreateMany<Guid>().ToList();
_mockUserRepository.Setup(r => r.FindByIdAsync(_userId))
.ReturnsAsync(user);
_mockUserNotification.Setup(n => n.GetNotifiedUsers(_userId))
.ReturnsAsync(friendsIds);
// Act
await _hub.OnDisconnectedAsync(null!);
// Assert
_mockOnlineUserStore.Verify(store => store.SetOfflineUser(_userId), Times.Once);
_mockUserRepository.Verify(r => r.UpdateAsync(It.Is<User>(u => u.Id == _userId && u.WasOnline > DateTime.MinValue)), Times.Once);
_clientsMock.Verify(c => c.Group(It.IsAny<string>()), Times.AtLeastOnce);
foreach (var friendId in friendsIds)
{
_clientProxyMock.Verify(proxy =>
proxy.SendCoreAsync("UserOffline",
It.Is<object[]>(args => args.Length == 1 && (Guid)args[0] == _userId),
It.IsAny<CancellationToken>()),
Times.Exactly(friendsIds.Count));
}
}
[Test]
public async Task OnDisconnectedAsync_UserIdEmpty_LogsAndSkips()
{
// Arrange
_mockHubUserAccessor.Setup(h => h.GetUserId(It.IsAny<HubCallerContext>(), true))
.Returns(Guid.Empty);
// Act
await _hub.OnDisconnectedAsync(null!);
// Assert
_mockOnlineUserStore.Verify(s => s.SetOfflineUser(It.IsAny<Guid>()), Times.Never);
_mockUserRepository.Verify(r => r.UpdateAsync(It.IsAny<User>()), Times.Never);
_clientProxyMock.Verify(proxy =>
proxy.SendCoreAsync("UserOffline",
It.Is<object[]>(args => args.Length == 1 && (Guid)args[0] == _userId),
It.IsAny<CancellationToken>()),
Times.Never);
}
[TearDown]
public void TearDown()
{
_hub?.Dispose();
}
}
@@ -1,11 +1,12 @@
using AutoFixture; using AutoFixture;
using Govor.API.Controllers; using Govor.API.Controllers.Authentication;
using Govor.API.Services.Authentication.Interfaces;
using Govor.Application.Exceptions.AuthService; using Govor.Application.Exceptions.AuthService;
using Govor.Application.Exceptions.InvitesService; using Govor.Application.Exceptions.InvitesService;
using Govor.Application.Interfaces.Authentication; using Govor.Application.Interfaces.Authentication;
using Govor.Application.Interfaces.UserSession;
using Govor.Contracts.Requests; using Govor.Contracts.Requests;
using Govor.Core.Models; using Govor.Core.Models;
using Govor.Core.Models.Users;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moq; using Moq;
@@ -19,6 +20,7 @@ public class AuthControllerTests
private Mock<IAccountService> _accountServiceMock; private Mock<IAccountService> _accountServiceMock;
private Mock<IInvitesService> _invitesServiceMock; private Mock<IInvitesService> _invitesServiceMock;
private Mock<ILogger<AuthController>> _loggerMock; private Mock<ILogger<AuthController>> _loggerMock;
private Mock<IUserSessionOpener> _userSessionOpenerMock;
private AuthController _controller; private AuthController _controller;
[SetUp] [SetUp]
@@ -31,10 +33,12 @@ public class AuthControllerTests
_accountServiceMock = new Mock<IAccountService>(); _accountServiceMock = new Mock<IAccountService>();
_invitesServiceMock = new Mock<IInvitesService>(); _invitesServiceMock = new Mock<IInvitesService>();
_loggerMock = new Mock<ILogger<AuthController>>(); _loggerMock = new Mock<ILogger<AuthController>>();
_userSessionOpenerMock = new Mock<IUserSessionOpener>();
_controller = new AuthController( _controller = new AuthController(
_accountServiceMock.Object, _accountServiceMock.Object,
_invitesServiceMock.Object, _invitesServiceMock.Object,
_userSessionOpenerMock.Object,
_loggerMock.Object _loggerMock.Object
); );
} }
@@ -46,10 +50,19 @@ public class AuthControllerTests
// Arrange // Arrange
var request = _fixture.Create<RegistrationRequest>(); var request = _fixture.Create<RegistrationRequest>();
var invitation = _fixture.Create<Invitation>(); var invitation = _fixture.Create<Invitation>();
var token = _fixture.Create<string>(); var token = _fixture.Create<RefreshResult>();
_invitesServiceMock.Setup(s => s.Validate(request.InviteLink)).Returns(invitation); var user = _fixture.Build<User>()
_accountServiceMock.Setup(s => s.RegistrationAsync(request.Name, request.Password, invitation)).ReturnsAsync(token); .With(x => x.Username).Create();
_invitesServiceMock.Setup(s => s.ValidateAsync(request.InviteLink)).ReturnsAsync(invitation);
_accountServiceMock.Setup(l => l.RegistrationAsync(request.Name, request.Password, invitation))
.ReturnsAsync(user);
_userSessionOpenerMock.Setup(f => f.OpenSessionAsync(user, request.DeviceInfo))
.ReturnsAsync(token);
// Act // Act
var result = await _controller.Register(request); var result = await _controller.Register(request);
@@ -57,8 +70,11 @@ public class AuthControllerTests
// Assert // Assert
Assert.That(result, Is.InstanceOf<OkObjectResult>()); Assert.That(result, Is.InstanceOf<OkObjectResult>());
var okResult = result as OkObjectResult; var okResult = result as OkObjectResult;
dynamic value = okResult.Value;
Assert.That((string)value.GetType().GetProperty("token").GetValue(value, null), Is.EqualTo(token)); var response = okResult?.Value as RefreshResult;
Assert.That(response, Is.Not.Null);
Assert.That(response.accessToken, Is.EqualTo(token.accessToken));
Assert.That(response.refreshToken, Is.EqualTo(token.refreshToken));
} }
[Test] [Test]
@@ -80,14 +96,14 @@ public class AuthControllerTests
{ {
// Arrange // Arrange
var request = _fixture.Create<RegistrationRequest>(); var request = _fixture.Create<RegistrationRequest>();
_invitesServiceMock.Setup(s => s.Validate(request.InviteLink)).Throws(new InviteLinkInvalidException(request.InviteLink)); _invitesServiceMock.Setup(s => s.ValidateAsync(request.InviteLink)).ThrowsAsync(new InviteLinkInvalidException(request.InviteLink));
// Act // Act
var result = await _controller.Register(request); var result = await _controller.Register(request);
// Assert // Assert
Assert.That(result, Is.InstanceOf<NotFoundObjectResult>()); Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
var notFoundObjectResult = result as NotFoundObjectResult; var notFoundObjectResult = result as BadRequestObjectResult;
Assert.That(notFoundObjectResult.Value, Is.EqualTo("Invite link invalid.")); Assert.That(notFoundObjectResult.Value, Is.EqualTo("Invite link invalid."));
} }
@@ -97,7 +113,7 @@ public class AuthControllerTests
// Arrange // Arrange
var request = _fixture.Create<RegistrationRequest>(); var request = _fixture.Create<RegistrationRequest>();
var invitation = _fixture.Create<Invitation>(); var invitation = _fixture.Create<Invitation>();
_invitesServiceMock.Setup(s => s.Validate(request.InviteLink)).Returns(invitation); _invitesServiceMock.Setup(s => s.ValidateAsync(request.InviteLink)).ReturnsAsync(invitation);
_accountServiceMock.Setup(s => s.RegistrationAsync(request.Name, request.Password, invitation)) _accountServiceMock.Setup(s => s.RegistrationAsync(request.Name, request.Password, invitation))
.ThrowsAsync(new UserAlreadyExistException(request.Name)); .ThrowsAsync(new UserAlreadyExistException(request.Name));
@@ -116,7 +132,7 @@ public class AuthControllerTests
// Arrange // Arrange
var request = _fixture.Create<RegistrationRequest>(); var request = _fixture.Create<RegistrationRequest>();
var invitation = _fixture.Create<Invitation>(); var invitation = _fixture.Create<Invitation>();
_invitesServiceMock.Setup(s => s.Validate(request.InviteLink)).Returns(invitation); _invitesServiceMock.Setup(s => s.ValidateAsync(request.InviteLink)).ReturnsAsync(invitation);
_accountServiceMock.Setup(s => s.RegistrationAsync(request.Name, request.Password, invitation)) _accountServiceMock.Setup(s => s.RegistrationAsync(request.Name, request.Password, invitation))
.ThrowsAsync(new System.Exception("Generic error")); .ThrowsAsync(new System.Exception("Generic error"));
@@ -136,16 +152,26 @@ public class AuthControllerTests
{ {
// Arrange // Arrange
var loginRequest = _fixture.Create<LoginRequest>(); var loginRequest = _fixture.Create<LoginRequest>();
var token = _fixture.Create<string>(); var token = _fixture.Create<RefreshResult>();
_accountServiceMock.Setup(l => l.LoginAsync(loginRequest.Name, loginRequest.Password)).ReturnsAsync(token);
var user = _fixture.Build<User>()
.With(x => x.Username).Create();
_accountServiceMock.Setup(l => l.LoginAsync(loginRequest.Name, loginRequest.Password)).ReturnsAsync(user);
_userSessionOpenerMock.Setup(f => f.OpenSessionAsync(user, loginRequest.DeviceInfo))
.ReturnsAsync(token);
// Act // Act
var result = await _controller.Login(loginRequest); var result = await _controller.Login(loginRequest);
// Assert // Assert
Assert.That(result, Is.InstanceOf<OkObjectResult>()); Assert.That(result, Is.InstanceOf<OkObjectResult>());
var okResult = result as OkObjectResult; var okResult = result as OkObjectResult;
dynamic value = okResult.Value;
Assert.That((string)value.GetType().GetProperty("token").GetValue(value, null), Is.EqualTo(token)); var response = okResult?.Value as RefreshResult;
Assert.That(response, Is.Not.Null);
Assert.That(response.accessToken, Is.EqualTo(token.accessToken));
Assert.That(response.refreshToken, Is.EqualTo(token.refreshToken));
} }
[Test] [Test]
@@ -0,0 +1,266 @@
using AutoFixture;
using AutoMapper;
using Govor.API.Controllers;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Contracts.Requests;
using Govor.Contracts.Responses;
using Govor.Core.Models.Messages;
using Govor.Data.Repositories.Exceptions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.API.Tests.IntegrationTests.Controllers;
[TestFixture]
public class ChatLoadControllerTests
{
private Mock<ICurrentUserService> _currentUserServiceMock;
private Mock<ILogger<ChatLoadController>> _loggerMock;
private Mock<IMessagesLoader> _messagesLoaderMock;
private Mock<IMapper> _mapperMock;
private Fixture _fixture;
private ChatLoadController _controller;
private Guid _userId = Guid.NewGuid();
private Guid _chatId = Guid.NewGuid();
private Guid _currentId = Guid.NewGuid();
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_currentUserServiceMock = new Mock<ICurrentUserService>();
_loggerMock = new Mock<ILogger<ChatLoadController>>();
_messagesLoaderMock = new Mock<IMessagesLoader>();
_mapperMock = new Mock<IMapper>();
_currentUserServiceMock.Setup(u => u.GetCurrentUserId()).Returns(_currentId);
_controller = new ChatLoadController(
_loggerMock.Object,
_messagesLoaderMock.Object,
_currentUserServiceMock.Object,
_mapperMock.Object);
}
// Tests for GetChatMessages action
[Test]
public async Task GetChatMessages_ValidRequest_ReturnsOkResult()
{
// Arrange
var random = new Random();
var messages = _fixture.CreateMany<Message>(random.Next(3, 10)).ToList();
var before = random.Next(1, 10);
var after = random.Next(1, 10);
var query = new MessageQuery()
{
Before = before,
After = after,
StartMessageId = null
};
_messagesLoaderMock.Setup(m =>
m.LoadMessagesInChatGroup(_chatId, _currentId, null, before, after))
.ReturnsAsync(messages);
var messagesResponse = messages.Select(m => new MessageResponse()
{
Id = m.Id,
SenderId = m.SenderId,
RecipientId = m.RecipientId,
RecipientType = m.RecipientType,
}).ToList();
_mapperMock.Setup(f => f.Map<List<MessageResponse>>(messages)).
Returns(messagesResponse);
// Act
var result = await _controller.GetGroupMessages(_chatId, query);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<OkObjectResult>());
var okResult = (OkObjectResult)result;
var values = okResult.Value as List<MessageResponse>;
Assert.That(values, Is.Not.Null);
Assert.That(values.Select(v => v.Id),
Is.EquivalentTo(messagesResponse.Select(m => m.Id)));
Assert.That(values.Select(v => v.SenderId),
Is.EquivalentTo(messagesResponse.Select(m => m.SenderId)));
Assert.That(values.Select(v => v.RecipientId),
Is.EquivalentTo(messagesResponse.Select(m => m.RecipientId)));
Assert.That(values.Select(v => v.RecipientType),
Is.EquivalentTo(messagesResponse.Select(m => m.RecipientType)));
}
[Test]
public async Task GetGroupMessages_InvalidQuery_ReturnsBadRequest()
{
// Arrange
var query = new MessageQuery { Before = -1, After = 10 };
// Act
var result = await _controller.GetGroupMessages(_chatId, query);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
}
[Test]
public async Task GetGroupMessages_UnauthorizedAccess_ReturnsForbid()
{
// Arrange
var query = new MessageQuery { Before = 10, After = 10 };
_messagesLoaderMock.Setup(m => m.LoadMessagesInChatGroup(It.IsAny<Guid>(), It.IsAny<Guid>(), null, It.IsAny<int>(), It.IsAny<int>()))
.ThrowsAsync(new UnauthorizedAccessException());
// Act
var result = await _controller.GetGroupMessages(_chatId, query);
// Assert
Assert.That(result, Is.InstanceOf<ForbidResult>());
}
[Test]
public async Task GetGroupMessages_NotFound_ReturnsBadRequest()
{
// Arrange
var query = new MessageQuery { Before = 10, After = 10 };
_messagesLoaderMock.Setup(m => m.LoadMessagesInChatGroup(It.IsAny<Guid>(), It.IsAny<Guid>(), null, It.IsAny<int>(), It.IsAny<int>()))
.ThrowsAsync(new NotFoundException("Chat not found"));
// Act
var result = await _controller.GetGroupMessages(_chatId, query);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
}
[Test]
public async Task GetGroupMessages_ArgumentException_ReturnsBadRequest()
{
// Arrange
var query = new MessageQuery { Before = 10, After = 10 };
_messagesLoaderMock.Setup(m => m.LoadMessagesInChatGroup(It.IsAny<Guid>(), It.IsAny<Guid>(), null, It.IsAny<int>(), It.IsAny<int>()))
.ThrowsAsync(new ArgumentException("Invalid argument"));
// Act
var result = await _controller.GetGroupMessages(_chatId, query);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
}
// GetUserMessages tests
[Test]
public async Task GetUserMessages_ValidRequest_ReturnsOkResult()
{
// Arrange
var messages = _fixture.CreateMany<Message>(5).ToList();
var query = new MessageQuery { Before = 5, After = 5 };
_messagesLoaderMock.Setup(m => m.LoadMessagesInUserChat(_userId, _currentId, null, 5, 5))
.ReturnsAsync(messages);
var messageResponses = _fixture.CreateMany<MessageResponse>(5).ToList();
_mapperMock.Setup(m => m.Map<List<MessageResponse>>(messages)).Returns(messageResponses);
// Act
var result = await _controller.GetUserMessages(_userId, query);
// Assert
Assert.That(result, Is.InstanceOf<OkObjectResult>());
var okResult = result as OkObjectResult;
Assert.That(okResult.Value, Is.EqualTo(messageResponses));
}
[Test]
public async Task GetUserMessages_InvalidQuery_ReturnsBadRequest()
{
// Arrange
var query = new MessageQuery { Before = -1, After = 10 };
// Act
var result = await _controller.GetUserMessages(_userId, query);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
}
[Test]
public async Task GetUserMessages_InvalidOperation_ReturnsBadRequest()
{
// Arrange
var query = new MessageQuery { Before = 10, After = 10 };
_messagesLoaderMock.Setup(m => m.LoadMessagesInUserChat(It.IsAny<Guid>(), It.IsAny<Guid>(), null, It.IsAny<int>(), It.IsAny<int>()))
.ThrowsAsync(new InvalidOperationException());
// Act
var result = await _controller.GetUserMessages(_userId, query);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
}
[Test]
public async Task GetUserMessages_UnauthorizedAccess_ReturnsForbid()
{
// Arrange
var query = new MessageQuery { Before = 10, After = 10 };
_messagesLoaderMock.Setup(m => m.LoadMessagesInUserChat(It.IsAny<Guid>(), It.IsAny<Guid>(), null, It.IsAny<int>(), It.IsAny<int>()))
.ThrowsAsync(new UnauthorizedAccessException());
// Act
var result = await _controller.GetUserMessages(_userId, query);
// Assert
Assert.That(result, Is.InstanceOf<ForbidResult>());
}
[Test]
public async Task GetUserMessages_NotFound_ReturnsBadRequest()
{
// Arrange
var query = new MessageQuery { Before = 10, After = 10 };
_messagesLoaderMock.Setup(m => m.LoadMessagesInUserChat(It.IsAny<Guid>(), It.IsAny<Guid>(), null, It.IsAny<int>(), It.IsAny<int>()))
.ThrowsAsync(new NotFoundException("User not found"));
// Act
var result = await _controller.GetUserMessages(_userId, query);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
}
[Test]
public async Task GetUserMessages_ArgumentException_ReturnsBadRequest()
{
// Arrange
var query = new MessageQuery { Before = 10, After = 10 };
_messagesLoaderMock.Setup(m => m.LoadMessagesInUserChat(It.IsAny<Guid>(), It.IsAny<Guid>(), null, It.IsAny<int>(), It.IsAny<int>()))
.ThrowsAsync(new ArgumentException("Invalid argument"));
// Act
var result = await _controller.GetUserMessages(_userId, query);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
}
[TearDown]
public void TearDown()
{
_controller?.Dispose();
}
}
@@ -1,115 +0,0 @@
using AutoFixture;
using Govor.API.Controllers;
using Govor.Application.Exceptions.FriendsService;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Contracts.DTOs;
using Govor.Core.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.API.Tests.IntegrationTests.Controllers;
[TestFixture]
public class FriendsControllerTests
{
private Fixture _fixture;
private Mock<ILogger<FriendsController>> _loggerMock;
private Mock<IFriendsService> _friendsServiceMock;
private Mock<ICurrentUserService> _currentUserServiceMock;
private FriendsController _controller;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_loggerMock = new Mock<ILogger<FriendsController>>();
_friendsServiceMock = new Mock<IFriendsService>();
_currentUserServiceMock = new Mock<ICurrentUserService>();
_controller = new FriendsController(
_loggerMock.Object,
_friendsServiceMock.Object,
_currentUserServiceMock.Object
);
}
// Tests for Search action
[Test]
public async Task Search_ValidRequest_ReturnsOkResult()
{
var users = _fixture.CreateMany<User>().ToList();
var userId = _fixture.Create<Guid>();
var query = _fixture.Create<string>();
_currentUserServiceMock.Setup(c => c.GetCurrentUserId()).Returns(userId);
_friendsServiceMock.Setup(f => f.SearchUsersAsync(query, userId))
.ReturnsAsync(users);
// Act
var result = await _controller.Search(query);
var okResult = result as OkObjectResult;
dynamic value = okResult.Value;
List<UserDto> userDtos = value as List<UserDto>;
// Assert
Assert.That(value, Is.Not.Null);
Assert.That(value.Count, Is.EqualTo(users.Count));
Assert.That(userDtos.Select(u => u.Id), Is.EqualTo(users.Select(u => u.Id)));
}
[Test]
public async Task Search_InvalidQuery_BadRequest()
{
// Act
var result = await _controller.Search(string.Empty);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
var badRequestResult = result as BadRequestObjectResult;
Assert.That(badRequestResult.Value, Is.EqualTo("Query cannot be empty"));
}
[Test]
public async Task Search_NotFound_IfThrowsSearchUsersException()
{
// Arrange
_friendsServiceMock.Setup(f => f.SearchUsersAsync(It.IsAny<string>(), It.IsAny<Guid>()))
.ThrowsAsync(new SearchUsersException(_fixture.Create<string>()));
// Act
var result = await _controller.Search(_fixture.Create<string>());
// Assert
Assert.That(result, Is.InstanceOf<NotFoundObjectResult>());
}
[Test]
public async Task Search_StatusCode500_IfThrowsSomeException()
{
// Arrange
_friendsServiceMock.Setup(f => f.SearchUsersAsync(It.IsAny<string>(), It.IsAny<Guid>()))
.ThrowsAsync(new Exception(_fixture.Create<string>()));
// Act
var result = await _controller.Search(_fixture.Create<string>());
// Assert
Assert.That(result, Is.InstanceOf<ObjectResult>());
var objectResult = result as ObjectResult;
Assert.That(objectResult.StatusCode, Is.EqualTo(500));
}
[TearDown]
public void TearDown()
{
_controller.Dispose();
}
}
@@ -0,0 +1,208 @@
using AutoFixture;
using AutoMapper;
using Govor.API.Controllers.Friends;
using Govor.Application.Interfaces.Friends;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Contracts.DTOs;
using Govor.Core.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.API.Tests.IntegrationTests.Controllers;
[TestFixture]
public class FriendsRequestQueryControllerTests
{
private Fixture _fixture;
private Mock<ILogger<FriendsRequestQueryController>> _loggerMock;
private Mock<IFriendRequestQueryService> _friendsServiceMock;
private Mock<ICurrentUserService> _currentUserServiceMock;
private Mock<IMapper> _mapperMock;
private FriendsRequestQueryController _controller;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_loggerMock = new Mock<ILogger<FriendsRequestQueryController>>();
_friendsServiceMock = new Mock<IFriendRequestQueryService>();
_currentUserServiceMock = new Mock<ICurrentUserService>();
_mapperMock = new Mock<IMapper>();
_controller = new FriendsRequestQueryController(
_loggerMock.Object,
_friendsServiceMock.Object,
_currentUserServiceMock.Object,
_mapperMock.Object
);
}
[Test]
public async Task GetIncomingRequests_ValidRequest_ReturnsOkResult()
{
// Arrange
var currentId = _fixture.Create<Guid>();
var friendships = _fixture.CreateMany<Friendship>().ToList();
var dtos = friendships.Select(f => new FriendshipDto()
{
AddresseeId = f.AddresseeId,
RequesterId = f.RequesterId
}).ToList();
_currentUserServiceMock.Setup(c => c.GetCurrentUserId())
.Returns(currentId);
_friendsServiceMock.Setup(f => f.GetIncomingAsync(currentId))
.ReturnsAsync(friendships);
_mapperMock.Setup(f => f.Map<List<FriendshipDto>>(friendships)).Returns(dtos);
// Act
var result = await _controller.GetIncomingRequests();
// Assert
Assert.That(result, Is.InstanceOf<OkObjectResult>());
var okResult = result as OkObjectResult;
List<FriendshipDto> value = okResult.Value as List<FriendshipDto>;
Assert.That(value, Is.Not.Null);
Assert.That(value.Count, Is.EqualTo(friendships.Count));
Assert.That(value.Select(f => f.AddresseeId), Is.EquivalentTo(friendships.Select(f => f.AddresseeId)));
Assert.That(value.Select(f => f.RequesterId), Is.EquivalentTo(friendships.Select(f => f.RequesterId)));
}
[Test]
public async Task GetIncomingRequests_Throws_InvalidOperationException_ReturnsBadRequest()
{
// Arrange
var currentId = _fixture.Create<Guid>();
_currentUserServiceMock.Setup(c => c.GetCurrentUserId())
.Returns(currentId);
_friendsServiceMock.Setup(f => f.GetIncomingAsync(currentId))
.ThrowsAsync(new InvalidOperationException());
// Act
var result = await _controller.GetIncomingRequests();
// Assert
Assert.That(result, Is.InstanceOf<OkObjectResult>());
var okResult = result as OkObjectResult;
List<FriendshipDto> value = okResult.Value as List<FriendshipDto>;
Assert.That(value, Is.Not.Null);
Assert.That(value.Count, Is.EqualTo(0));
}
[Test]
public async Task GetIncomingRequest_StatusCode500_IfThrowsSomeException()
{
// Arrange
var currentId = _fixture.Create<Guid>();
_currentUserServiceMock.Setup(c => c.GetCurrentUserId())
.Returns(currentId);
_friendsServiceMock.Setup(f => f.GetIncomingAsync(currentId))
.ThrowsAsync(new Exception());
// Act
var result = await _controller.GetIncomingRequests();
// Assert
Assert.That(result, Is.InstanceOf<ObjectResult>());
var objectResult = result as ObjectResult;
Assert.That(objectResult.StatusCode, Is.EqualTo(500));
}
// Test for GetResponses action
[Test]
public async Task GetResponses_ValidRequest_ReturnsOkResult()
{
// Arrange
var currentId = _fixture.Create<Guid>();
var friendships = _fixture.CreateMany<Friendship>().ToList();
var dtos = friendships.Select(f => new FriendshipDto()
{
AddresseeId = f.AddresseeId,
RequesterId = f.RequesterId
}).ToList();
_currentUserServiceMock.Setup(c => c.GetCurrentUserId())
.Returns(currentId);
_friendsServiceMock.Setup(f => f.GetResponsesAsync(currentId))
.ReturnsAsync(friendships);
_mapperMock.Setup(f => f.Map<List<FriendshipDto>>(friendships)).Returns(dtos);
// Act
var result = await _controller.GetResponses();
// Assert
Assert.That(result, Is.InstanceOf<OkObjectResult>());
var okResult = result as OkObjectResult;
List<FriendshipDto> value = okResult.Value as List<FriendshipDto>;
Assert.That(value, Is.Not.Null);
Assert.That(value.Count, Is.EqualTo(friendships.Count));
Assert.That(value.Select(f => f.AddresseeId), Is.EquivalentTo(friendships.Select(f => f.AddresseeId)));
Assert.That(value.Select(f => f.RequesterId), Is.EquivalentTo(friendships.Select(f => f.RequesterId)));
}
[Test]
public async Task GetResponses_Throws_InvalidOperationException_ReturnsEmptyList()
{
// Arrange
var currentId = _fixture.Create<Guid>();
_currentUserServiceMock.Setup(c => c.GetCurrentUserId())
.Returns(currentId);
_friendsServiceMock.Setup(f => f.GetResponsesAsync(currentId))
.ThrowsAsync(new InvalidOperationException());
// Act
var result = await _controller.GetResponses();
// Assert
Assert.That(result, Is.InstanceOf<OkObjectResult>());
var okResult = result as OkObjectResult;
List<FriendshipDto> value = okResult.Value as List<FriendshipDto>;
Assert.That(value, Is.Not.Null);
Assert.That(value.Count, Is.EqualTo(0));
}
[Test]
public async Task GetResponses_StatusCode500_IfThrowsSomeException()
{
// Arrange
var currentId = _fixture.Create<Guid>();
_currentUserServiceMock.Setup(c => c.GetCurrentUserId())
.Returns(currentId);
_friendsServiceMock.Setup(f => f.GetResponsesAsync(currentId))
.ThrowsAsync(new Exception());
// Act
var result = await _controller.GetResponses();
// Assert
Assert.That(result, Is.InstanceOf<ObjectResult>());
var objectResult = result as ObjectResult;
Assert.That(objectResult.StatusCode, Is.EqualTo(500));
}
[TearDown]
public void TearDown()
{
_controller?.Dispose();
}
}
@@ -0,0 +1,177 @@
using AutoFixture;
using AutoMapper;
using Govor.API.Controllers.Friends;
using Govor.Application.Exceptions.FriendsService;
using Govor.Application.Interfaces.Friends;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Contracts.DTOs;
using Govor.Core.Models.Users;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.API.Tests.IntegrationTests.Controllers;
[TestFixture]
public class FriendshipControllerTests
{
private Fixture _fixture;
private Mock<ILogger<FriendshipController>> _loggerMock;
private Mock<IFriendshipService> _friendsServiceMock;
private Mock<ICurrentUserService> _currentUserServiceMock;
private Mock<IMapper> _mapperMock;
private FriendshipController _controller;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_loggerMock = new Mock<ILogger<FriendshipController>>();
_friendsServiceMock = new Mock<IFriendshipService>();
_currentUserServiceMock = new Mock<ICurrentUserService>();
_mapperMock = new Mock<IMapper>();
_controller = new FriendshipController(
_loggerMock.Object,
_friendsServiceMock.Object,
_currentUserServiceMock.Object,
_mapperMock.Object
);
}
// Tests for Search action
[Test]
public async Task Search_ValidRequest_ReturnsOkResult()
{
var users = _fixture.CreateMany<User>().ToList();
var dtos = users.Select(u => new UserDto { Id = u.Id }).ToList();
var userId = _fixture.Create<Guid>();
var query = _fixture.Create<string>();
_currentUserServiceMock.Setup(c => c.GetCurrentUserId()).Returns(userId);
_friendsServiceMock.Setup(f => f.SearchUsersAsync(query, userId))
.ReturnsAsync(users);
_mapperMock.Setup(m => m.Map<List<UserDto>>(users)).Returns(dtos);
// Act
var result = await _controller.Search(query);
var okResult = result as OkObjectResult;
dynamic value = okResult.Value;
List<UserDto> userDtos = value as List<UserDto>;
// Assert
Assert.That(value, Is.Not.Null);
Assert.That(value.Count, Is.EqualTo(users.Count));
Assert.That(userDtos.Select(u => u.Id), Is.EqualTo(users.Select(u => u.Id)));
}
[Test]
public async Task Search_InvalidQuery_BadRequest()
{
// Act
var result = await _controller.Search(string.Empty);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
var badRequestResult = result as BadRequestObjectResult;
Assert.That(badRequestResult.Value, Is.EqualTo("Query cannot be empty"));
}
[Test]
public async Task Search_StatusCode500_IfThrowsSomeException()
{
// Arrange
_friendsServiceMock.Setup(f => f.SearchUsersAsync(It.IsAny<string>(), It.IsAny<Guid>()))
.ThrowsAsync(new Exception(_fixture.Create<string>()));
// Act
var result = await _controller.Search(_fixture.Create<string>());
// Assert
Assert.That(result, Is.InstanceOf<ObjectResult>());
var objectResult = result as ObjectResult;
Assert.That(objectResult.StatusCode, Is.EqualTo(500));
}
[Test]
public async Task GetFriends_ValidRequest_ReturnsOkResult()
{
// Arrange
var currentId = _fixture.Create<Guid>();
var users = _fixture.CreateMany<User>().ToList();
var dtos = users.Select(u => new UserDto { Id = u.Id }).ToList();
_currentUserServiceMock.Setup(c => c.GetCurrentUserId())
.Returns(currentId);
_friendsServiceMock.Setup(f => f.GetFriendsAsync(currentId))
.ReturnsAsync(users);
_mapperMock.Setup(m => m.Map<List<UserDto>>(users)).Returns(dtos);
// Act
var result = await _controller.GetFriends();
// Assert
Assert.That(result, Is.InstanceOf<OkObjectResult>());
var okResult = result as OkObjectResult;
List<UserDto> value = okResult.Value as List<UserDto>;
Assert.That(value, Is.Not.Null);
Assert.That(value.Count, Is.EqualTo(users.Count));
Assert.That(value.Select(u => u.Id), Is.EqualTo(users.Select(u => u.Id)));
}
[Test]
public async Task GetFriends_InvalidOperationException_ReturnsBadRequest()
{
// Arrange
var currentId = _fixture.Create<Guid>();
var users = _fixture.CreateMany<User>().ToList();
_currentUserServiceMock.Setup(c => c.GetCurrentUserId())
.Returns(currentId);
_friendsServiceMock.Setup(f => f.GetFriendsAsync(currentId))
.ThrowsAsync(new InvalidOperationException());
// Act
var result = await _controller.GetFriends();
// Assert
Assert.That(result, Is.InstanceOf<OkObjectResult>());
}
[Test]
public async Task GetFriends_Exception_ReturnsStatusCode500()
{
// Arrange
var currentId = _fixture.Create<Guid>();
var users = _fixture.CreateMany<User>().ToList();
_currentUserServiceMock.Setup(c => c.GetCurrentUserId())
.Returns(currentId);
_friendsServiceMock.Setup(f => f.GetFriendsAsync(currentId))
.ThrowsAsync(new Exception());
// Act
var result = await _controller.GetFriends();
// Assert
Assert.That(result, Is.InstanceOf<ObjectResult>());
var objectResult = result as ObjectResult;
Assert.That(objectResult.StatusCode, Is.EqualTo(500));
}
[TearDown]
public void TearDown()
{
_controller.Dispose();
}
}
@@ -0,0 +1,362 @@
using System.Text;
using AutoFixture;
using Govor.API.Controllers;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Application.Interfaces.Medias;
using Govor.Contracts.Requests;
using Govor.Core.Models.Messages;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.API.Tests.IntegrationTests.Controllers;
[TestFixture]
public class MediaControllerTests
{
private Fixture _fixture;
private Mock<ICurrentUserService> _currentUserMock;
private Mock<ILogger<MediaController>> _loggerMock;
private Mock<IMediaService> _mockMedia;
private Mock<IAccesserToDownloadMedia> _mockAccesser;
private MediaController _controller;
private Guid _userId = Guid.NewGuid();
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_currentUserMock = new Mock<ICurrentUserService>();
_loggerMock = new Mock<ILogger<MediaController>>();
_mockAccesser = new Mock<IAccesserToDownloadMedia>();
_mockMedia = new Mock<IMediaService>();
_currentUserMock.Setup(f => f.GetCurrentUserId()).Returns(_userId);
_controller = new MediaController(
_loggerMock.Object,
_mockMedia.Object,
_mockAccesser.Object,
_currentUserMock.Object);
}
// Tests for Upload action
[Test]
public async Task Upload_ValidRequest_ReturnsOkResult()
{
// Arrange
var content = "fake file content";
var fileName = "testfile.txt";
var fileBytes = System.Text.Encoding.UTF8.GetBytes(content);
var stream = new MemoryStream(fileBytes);
var formFileMock = new Mock<IFormFile>();
formFileMock.Setup(f => f.Length).Returns(fileBytes.Length);
formFileMock.Setup(f => f.FileName).Returns(fileName);
formFileMock.Setup(f => f.OpenReadStream()).Returns(stream);
formFileMock.Setup(f => f.CopyToAsync(It.IsAny<Stream>(), default))
.Returns<Stream, CancellationToken>((target, _) => stream.CopyToAsync(target));
var uploadRequest = _fixture.Build<MediaUploadRequest>()
.With(r => r.FromFile, formFileMock.Object)
.With(r => r.Type, MediaType.Image)
.With(r => r.MimeType, "image/png")
.With(r => r.EncryptedKey, "secret")
.Create();
var uploadResult = _fixture.Create<MediaUploadResult>();
_mockMedia.Setup(f => f.UploadMediaAsync(It.IsAny<Media>()))
.ReturnsAsync(uploadResult);
// Act
var result = await _controller.Upload(uploadRequest);
// Assert
Assert.That(result, Is.InstanceOf<OkObjectResult>());
var okResult = result as OkObjectResult;
var value = okResult?.Value as MediaUploadResult;
Assert.That(value, Is.Not.Null);
Assert.That(value!.Url, Is.EqualTo(uploadResult.Url));
Assert.That(value.MediaId, Is.EqualTo(uploadResult.MediaId));
}
[Test]
public async Task Upload_InvalidModelState_ReturnsBadRequest()
{
// Arrange
_controller.ModelState.AddModelError("Error", "Invalid model state");
var content = "fake file content";
var fileName = "testfile.txt";
var fileBytes = System.Text.Encoding.UTF8.GetBytes(content);
var stream = new MemoryStream(fileBytes);
var formFileMock = new Mock<IFormFile>();
formFileMock.Setup(f => f.Length).Returns(fileBytes.Length);
formFileMock.Setup(f => f.FileName).Returns(fileName);
formFileMock.Setup(f => f.OpenReadStream()).Returns(stream);
formFileMock.Setup(f => f.CopyToAsync(It.IsAny<Stream>(), default))
.Returns<Stream, CancellationToken>((target, _) => stream.CopyToAsync(target));
var uploadRequest = _fixture.Build<MediaUploadRequest>()
.With(r => r.FromFile, formFileMock.Object)
.With(r => r.Type, MediaType.Image)
.With(r => r.MimeType, "image/png")
.With(r => r.EncryptedKey, "secret")
.Create();
// Act
var result = await _controller.Upload(uploadRequest);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
}
[Test]
public async Task Upload_NoFileUploaded_ReturnsBadRequest()
{
// Arrange
var uploadRequest = _fixture.Build<MediaUploadRequest>()
.With(r => r.FromFile, (IFormFile)null)
.Create();
// Act
var result = await _controller.Upload(uploadRequest);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
var badRequestResult = result as BadRequestObjectResult;
Assert.That(badRequestResult?.Value, Is.EqualTo("No file uploaded"));
}
[Test]
public async Task Upload_EmptyFile_ReturnsBadRequest()
{
// Arrange
var formFileMock = new Mock<IFormFile>();
formFileMock.Setup(f => f.Length).Returns(0);
var uploadRequest = _fixture.Build<MediaUploadRequest>()
.With(r => r.FromFile, formFileMock.Object)
.Create();
// Act
var result = await _controller.Upload(uploadRequest);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
var badRequestResult = result as BadRequestObjectResult;
Assert.That(badRequestResult?.Value, Is.EqualTo("No file uploaded"));
}
[Test]
public async Task Upload_FileTooLarge_ReturnsBadRequest()
{
// Arrange
var formFileMock = new Mock<IFormFile>();
formFileMock.Setup(f => f.Length).Returns(20_000_001); // Just over 20MB
var uploadRequest = _fixture.Build<MediaUploadRequest>()
.With(r => r.FromFile, formFileMock.Object)
.Create();
// Act
var result = await _controller.Upload(uploadRequest);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
var badRequestResult = result as BadRequestObjectResult;
Assert.That(badRequestResult?.Value, Is.EqualTo("File is too large"));
}
[Test]
public async Task Upload_MissingMimeType_ReturnsBadRequest()
{
// Arrange
var formFileMock = new Mock<IFormFile>();
formFileMock.Setup(f => f.Length).Returns(1000);
formFileMock.Setup(f => f.FileName).Returns("testfile.txt");
var uploadRequest = _fixture.Build<MediaUploadRequest>()
.With(r => r.FromFile, formFileMock.Object)
.With(r => r.MimeType, string.Empty)
.Create();
// Act
var result = await _controller.Upload(uploadRequest);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
var badRequestResult = result as BadRequestObjectResult;
Assert.That(badRequestResult?.Value, Is.EqualTo("Missing MIME type"));
}
[Test]
public async Task Upload_UnauthorizedAccess_ReturnsUnauthorized()
{
// Arrange
var formFileMock = new Mock<IFormFile>();
formFileMock.Setup(f => f.Length).Returns(1000);
formFileMock.Setup(f => f.FileName).Returns("testfile.txt");
formFileMock.Setup(f => f.OpenReadStream()).Returns(new MemoryStream(new byte[1000]));
var uploadRequest = _fixture.Build<MediaUploadRequest>()
.With(r => r.FromFile, formFileMock.Object)
.With(r => r.MimeType, "text/plain")
.Create();
_mockMedia.Setup(f => f.UploadMediaAsync(It.IsAny<Media>()))
.ThrowsAsync(new UnauthorizedAccessException("Access denied"));
// Act
var result = await _controller.Upload(uploadRequest);
// Assert
Assert.That(result, Is.InstanceOf<ForbidResult>());
var forbidResult = result as ForbidResult;
Assert.That(forbidResult.AuthenticationSchemes.First(), Is.EqualTo("Access denied"));
}
[Test]
public async Task Upload_InvalidOperation_ReturnsBadRequest()
{
// Arrange
var formFileMock = new Mock<IFormFile>();
formFileMock.Setup(f => f.Length).Returns(1000);
formFileMock.Setup(f => f.FileName).Returns("testfile.txt");
formFileMock.Setup(f => f.OpenReadStream()).Returns(new MemoryStream(new byte[1000]));
var uploadRequest = _fixture.Build<MediaUploadRequest>()
.With(r => r.FromFile, formFileMock.Object)
.With(r => r.MimeType, "text/plain")
.Create();
_mockMedia.Setup(f => f.UploadMediaAsync(It.IsAny<Media>()))
.ThrowsAsync(new InvalidOperationException("Invalid operation"));
// Act
var result = await _controller.Upload(uploadRequest);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
var badRequestResult = result as BadRequestObjectResult;
Assert.That(badRequestResult?.Value, Is.EqualTo("Invalid operation"));
}
[Test]
public async Task Upload_GeneralException_ReturnsInternalServerError()
{
// Arrange
var formFileMock = new Mock<IFormFile>();
formFileMock.Setup(f => f.Length).Returns(1000);
formFileMock.Setup(f => f.FileName).Returns("testfile.txt");
formFileMock.Setup(f => f.OpenReadStream()).Returns(new MemoryStream(new byte[1000]));
var uploadRequest = _fixture.Build<MediaUploadRequest>()
.With(r => r.FromFile, formFileMock.Object)
.With(r => r.MimeType, "text/plain")
.Create();
_mockMedia.Setup(f => f.UploadMediaAsync(It.IsAny<Media>()))
.ThrowsAsync(new Exception("Something went wrong"));
// Act
var result = await _controller.Upload(uploadRequest);
// Assert
Assert.That(result, Is.InstanceOf<ObjectResult>());
var objectResult = result as ObjectResult;
Assert.That(objectResult.StatusCode, Is.EqualTo(500));
}
// Tests for Download action
[Test]
public async Task Download_HasAccessAndMediaExists_ReturnsFile()
{
// Arrange
var mediaId = Guid.NewGuid();
var media = _fixture.Build<Media>()
.With(m => m.Data, Encoding.UTF8.GetBytes("fake file content"))
.With(m => m.MimeType, "application/octet-stream") // Ensure MimeType is set
.With(m => m.FileName, "testfile.txt")
.Create();
_mockAccesser.Setup(f => f.HasAccessAsync(mediaId, _userId)).ReturnsAsync(true);
_mockMedia.Setup(f => f.GetMediaByIdAsync(mediaId)).ReturnsAsync(media);
// Act
var result = await _controller.Download(mediaId);
// Assert
Assert.That(result, Is.InstanceOf<FileContentResult>());
var fileResult = result as FileContentResult;
Assert.That(fileResult?.FileContents, Is.EqualTo(media.Data));
Assert.That(fileResult?.ContentType, Is.EqualTo(media.MimeType)); // Changed MineType to MimeType
Assert.That(fileResult?.FileDownloadName, Is.EqualTo(Path.GetFileName(media.FileName)));
}
[Test]
public async Task Download_NoAccess_ReturnsForbid()
{
// Arrange
var mediaId = Guid.NewGuid();
_mockAccesser.Setup(f => f.HasAccessAsync(mediaId, _userId)).ReturnsAsync(false);
// Act
var result = await _controller.Download(mediaId);
// Assert
Assert.That(result, Is.InstanceOf<ForbidResult>());
}
[Test]
public async Task Download_MediaNotFound_ReturnsNotFound()
{
// Arrange
var mediaId = Guid.NewGuid();
_mockAccesser.Setup(f => f.HasAccessAsync(mediaId, _userId)).ReturnsAsync(true);
_mockMedia.Setup(f => f.GetMediaByIdAsync(mediaId))
.ThrowsAsync(new KeyNotFoundException("Media not found"));
// Act
var result = await _controller.Download(mediaId);
// Assert
Assert.That(result, Is.InstanceOf<NotFoundObjectResult>());
var notFoundResult = result as NotFoundObjectResult;
Assert.That(notFoundResult?.Value, Is.EqualTo("Media not found"));
}
[Test]
public async Task Download_GeneralException_ReturnsInternalServerError()
{
// Arrange
var mediaId = Guid.NewGuid();
_mockAccesser.Setup(f => f.HasAccessAsync(mediaId, _userId)).ReturnsAsync(true);
_mockMedia.Setup(f => f.GetMediaByIdAsync(mediaId))
.ThrowsAsync(new Exception("Something went wrong"));
// Act
var result = await _controller.Download(mediaId);
// Assert
Assert.That(result, Is.InstanceOf<ObjectResult>());
var objectResult = result as ObjectResult;
Assert.That(objectResult.StatusCode, Is.EqualTo(500));
}
[TearDown]
public void TearDown()
{
_controller.Dispose();
}
}
@@ -0,0 +1,138 @@
using Govor.API.Controllers;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.API.Tests.IntegrationTests.Controllers;
[TestFixture]
public class OnlinePingingControllerTests
{
private Mock<ILogger<OnlinePingingController>> _loggerMock;
private Mock<IPingHandlerService> _pingHandlerServiceMock;
private Mock<ICurrentUserService> _currentUserServiceMock;
private OnlinePingingController _controller;
[SetUp]
public void SetUp()
{
_loggerMock = new Mock<ILogger<OnlinePingingController>>();
_pingHandlerServiceMock = new Mock<IPingHandlerService>();
_currentUserServiceMock = new Mock<ICurrentUserService>();
_controller = new OnlinePingingController(_loggerMock.Object, _pingHandlerServiceMock.Object,
_currentUserServiceMock.Object);
}
[Test]
public async Task Ping_ValidUserId_ReturnsOk()
{
// Arrange
var userId = Guid.NewGuid();
_currentUserServiceMock.Setup(x => x.GetCurrentUserId()).Returns(userId);
_pingHandlerServiceMock.Setup(x => x.Ping(userId)).Returns(Task.CompletedTask);
// Act
var result = await _controller.Ping();
// Assert
Assert.That(result, Is.InstanceOf<OkResult>());
_pingHandlerServiceMock.Verify(x => x.Ping(userId), Times.Once());
_loggerMock.VerifyLog(LogLevel.Information, $"Ping from user {userId} processed successfully", Times.Once());
}
[Test]
public async Task Ping_InvalidOperationException_ReturnsBadRequest()
{
// Arrange
var userId = Guid.NewGuid();
var exception = new InvalidOperationException("User not found");
_currentUserServiceMock.Setup(x => x.GetCurrentUserId()).Returns(userId);
_pingHandlerServiceMock.Setup(x => x.Ping(userId)).ThrowsAsync(exception);
// Act
var result = await _controller.Ping();
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
var badRequestResult = (BadRequestObjectResult)result;
Assert.That(badRequestResult.Value, Is.EqualTo("User can't be found in our database."));
_loggerMock.VerifyLog(LogLevel.Error, exception.Message, Times.Once());
}
[Test]
public async Task Ping_UnauthorizedAccessException_ReturnsForbid()
{
// Arrange
var userId = Guid.NewGuid();
var exception = new UnauthorizedAccessException("Unauthorized");
_currentUserServiceMock.Setup(x => x.GetCurrentUserId()).Returns(userId);
_pingHandlerServiceMock.Setup(x => x.Ping(userId)).ThrowsAsync(exception);
// Act
var result = await _controller.Ping();
// Assert
Assert.That(result, Is.InstanceOf<ForbidResult>());
_loggerMock.VerifyLog(LogLevel.Error, exception.Message, Times.Once());
}
[Test]
public async Task Ping_GeneralException_ReturnsStatusCode500()
{
// Arrange
var userId = Guid.NewGuid();
var exception = new Exception("Unexpected error");
_currentUserServiceMock.Setup(x => x.GetCurrentUserId()).Returns(userId);
_pingHandlerServiceMock.Setup(x => x.Ping(userId)).ThrowsAsync(exception);
// Act
var result = await _controller.Ping();
// Assert
Assert.That(result, Is.InstanceOf<ObjectResult>());
var objectResult = (ObjectResult)result;
Assert.That(objectResult.StatusCode, Is.EqualTo(500));
_loggerMock.VerifyLog(LogLevel.Error, exception.Message, Times.Once());
}
[Test]
public async Task Ping_UnauthorizedUserId_ThrowsUnauthorizedAccessException()
{
// Arrange
var exception = new UnauthorizedAccessException("userID claim is missing or invalid");
_currentUserServiceMock.Setup(x => x.GetCurrentUserId()).Throws(exception);
// Act
var result = await _controller.Ping();
// Assert
Assert.That(result, Is.InstanceOf<ForbidResult>());
_loggerMock.VerifyLog(LogLevel.Error, exception.Message, Times.Once());
_pingHandlerServiceMock.Verify(x => x.Ping(It.IsAny<Guid>()), Times.Never());
}
[TearDown]
public void TearDown()
{
_controller?.Dispose();
}
}
// Helper extension for verifying logger calls
public static class LoggerMockExtensions
{
public static void VerifyLog<T>(this Mock<ILogger<T>> logger, LogLevel level, string message, Times times)
{
logger.Verify(
x => x.Log(
It.Is<LogLevel>(l => l == level),
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains(message)),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()),
times);
}
}
@@ -0,0 +1,284 @@
using System.Text;
using AutoMapper;
using Govor.API.Controllers;
using Govor.API.Hubs;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Application.Interfaces.Medias;
using Govor.Application.Profiles;
using Govor.Contracts.DTOs;
using Govor.Contracts.Requests;
using Govor.Core.Models;
using Govor.Core.Models.Messages;
using Govor.Data.Repositories.Exceptions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Moq;
using Newtonsoft.Json.Linq;
namespace Govor.API.Tests;
[TestFixture]
public class ProfileControllerTests
{
private Mock<ILogger<ProfileController>> _mockLogger = null!;
private Mock<IMediaService> _mockMediaService = null!;
private Mock<IProfileService> _mockProfileService = null!;
private Mock<ICurrentUserService> _mockCurrentUserService = null!;
private Mock<IHubContext<ProfileHub>> _mockProfileHubContext = null!;
private Mock<IHubClients> _mockClients = null!;
private Mock<IClientProxy> _mockClientProxy = null!;
private Mock<IMapper> _mockMapper = null!;
private ProfileController _controller = null!;
private Guid _userId;
[SetUp]
public void SetUp()
{
_mockLogger = new Mock<ILogger<ProfileController>>();
_mockMediaService = new Mock<IMediaService>();
_mockProfileService = new Mock<IProfileService>();
_mockCurrentUserService = new Mock<ICurrentUserService>();
_mockMapper = new Mock<IMapper>();
_mockProfileHubContext = new Mock<IHubContext<ProfileHub>>();
_mockClients = new Mock<IHubClients>();
_mockClientProxy = new Mock<IClientProxy>();
_mockProfileHubContext.Setup(x => x.Clients).Returns(_mockClients.Object);
_mockClients.Setup(x => x.All).Returns(_mockClientProxy.Object);
_mockClientProxy.Setup(x => x.SendCoreAsync(It.IsAny<string>(), It.IsAny<object?[]?>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
_controller = new ProfileController(
_mockLogger.Object,
_mockMediaService.Object,
_mockProfileService.Object,
_mockCurrentUserService.Object,
_mockProfileHubContext.Object,
_mockMapper.Object);
_userId = Guid.NewGuid();
_mockCurrentUserService.Setup(x => x.GetCurrentUserId()).Returns(_userId);
}
[Test]
public async Task UploadAvatar_ReturnsBadRequest_WhenFileIsEmpty()
{
// Arrange
var request = new AvatarUploadRequest
{
FromFile = null
};
// Act
var result = await _controller.UploadAvatar(request);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
var badRequestResult = (BadRequestObjectResult)result;
Assert.That(badRequestResult.Value, Is.EqualTo("File is empty."));
_mockMediaService.Verify(s => s.UploadMediaAsync(It.IsAny<Media>()), Times.Never);
}
[Test]
public async Task UploadAvatar_ReturnsBadRequest_WhenFileLengthIsZero()
{
// Arrange
var mockFile = new Mock<IFormFile>();
mockFile.Setup(f => f.Length).Returns(0);
var request = new AvatarUploadRequest
{
FromFile = mockFile.Object
};
// Act
var result = await _controller.UploadAvatar(request);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
var badRequestResult = (BadRequestObjectResult)result;
Assert.That(badRequestResult.Value, Is.EqualTo("File is empty."));
_mockMediaService.Verify(s => s.UploadMediaAsync(It.IsAny<Media>()), Times.Never);
}
[Test]
public async Task UploadAvatar_ReturnsOkAndPerformsAllActions_WhenFileIsValid()
{
// Arrange
var mediaId = Guid.NewGuid();
var expectedMediaInfo = new MediaUploadResult(mediaId, $"/media/{mediaId}");
var fileName = "test_avatar.jpg";
var fileContent = "This is a test image content";
var stream = new MemoryStream(Encoding.UTF8.GetBytes(fileContent));
var mockFile = new Mock<IFormFile>();
mockFile.Setup(f => f.FileName).Returns(fileName);
mockFile.Setup(f => f.Length).Returns(stream.Length);
mockFile.Setup(f => f.OpenReadStream()).Returns(stream);
mockFile.Setup(f => f.CopyToAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
.Callback<Stream, CancellationToken>((s, t) => stream.CopyTo(s)); // Для ReadFileAsync
var request = new AvatarUploadRequest
{
FromFile = mockFile.Object,
Type = MediaType.Image,
MimeType = "image/jpeg"
};
_mockMediaService.Setup(s => s.UploadMediaAsync(It.IsAny<Media>()))
.ReturnsAsync(expectedMediaInfo);
_mockProfileService.Setup(s => s.SetNewIcon(_userId, mediaId))
.Returns(Task.CompletedTask);
// Act
var result = await _controller.UploadAvatar(request);
// Assert
Assert.That(result, Is.InstanceOf<OkObjectResult>());
var okResult = (OkObjectResult)result;
Assert.That(okResult.StatusCode, Is.EqualTo(200));
Assert.That(okResult.Value, Is.EqualTo(expectedMediaInfo));
mockFile.Verify(f => f.CopyToAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()), Times.Once);
_mockMediaService.Verify(s => s.UploadMediaAsync(It.Is<Media>(m =>
m.OwnerId == _userId &&
m.OwnerType == MediaOwnerType.Avatar &&
m.FileName == fileName
)), Times.Once);
_mockProfileService.Verify(s => s.SetNewIcon(_userId, mediaId), Times.Once);
_mockClientProxy.Verify(
c => c.SendCoreAsync("AvatarUpdated", It.Is<object?[]>(
args => args.Length == 1 &&
JObject.FromObject(args[0]!).Value<Guid>("userId") == _userId &&
JObject.FromObject(args[0]!).Value<Guid>("iconId") == mediaId
), It.IsAny<CancellationToken>()),
Times.Once,
"Hub SendAsync must be called to notify clients."
);
}
[Test]
public async Task UploadAvatar_ReturnsInternalServerError_WhenMediaServiceFails()
{
// Arrange
var mockFile = new Mock<IFormFile>();
mockFile.Setup(f => f.Length).Returns(100);
mockFile.Setup(f => f.OpenReadStream()).Returns(new MemoryStream(Encoding.UTF8.GetBytes("test")));
var request = new AvatarUploadRequest { FromFile = mockFile.Object };
_mockMediaService.Setup(s => s.UploadMediaAsync(It.IsAny<Media>()))
.ThrowsAsync(new System.Exception("Simulated upload failure"));
// Act
var result = await _controller.UploadAvatar(request);
// Assert
Assert.That(result, Is.InstanceOf<ObjectResult>());
var objectResult = (ObjectResult)result;
Assert.That(objectResult.StatusCode, Is.EqualTo(500));
_mockProfileService.Verify(s => s.SetNewIcon(It.IsAny<Guid>(), It.IsAny<Guid>()), Times.Never);
_mockClientProxy.Verify(
c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object?[]?>(), It.IsAny<CancellationToken>()),
Times.Never,
"The Hub should not be called in case of a service error."
);
}
[Test]
public async Task DowloadProfile_ReturnsOk_WhenProfileExists()
{
// Arrange
var userProfile = new UserProfile
{
Id = _userId,
Username = "test_user",
Description = "test description",
IconId = Guid.NewGuid()
};
var userProfileDto = new UserProfileDto
{
Id = _userId,
Username = "test_user",
Description = "test description",
IconId = userProfile.IconId
};
_mockProfileService.Setup(s => s.GetUserProfileAsync(_userId))
.ReturnsAsync(userProfile);
_mockMapper.Setup(m => m.Map<UserProfileDto>(userProfile))
.Returns(userProfileDto);
// Act
var result = await _controller.DownloadProfile();
// Assert
var okResult = result as OkObjectResult;
Assert.That(okResult, Is.Not.Null);
Assert.That(okResult!.StatusCode, Is.EqualTo(200));
Assert.That(okResult.Value, Is.EqualTo(userProfileDto));
}
[Test]
public async Task DowloadProfile_ReturnsForbid_WhenUnauthorizedAccessExceptionThrown()
{
// Arrange
_mockProfileService.Setup(s => s.GetUserProfileAsync(_userId))
.ThrowsAsync(new UnauthorizedAccessException("Access denied"));
// Act
var result = await _controller.DownloadProfile();
// Assert
Assert.That(result, Is.InstanceOf<ForbidResult>());
}
[Test]
public async Task DowloadProfile_ReturnsBadRequest_WhenNotFoundExceptionThrown()
{
// Arrange
_mockProfileService.Setup(s => s.GetUserProfileAsync(_userId))
.ThrowsAsync(new NotFoundException("Cannot find profile"));
// Act
var result = await _controller.DownloadProfile();
// Assert
var notFoundResult = result as NotFoundObjectResult;
Assert.That(notFoundResult, Is.Not.Null);
Assert.That(notFoundResult!.Value, Is.EqualTo("Profile not found"));
}
[Test]
public async Task DowloadProfile_Returns500_WhenUnexpectedExceptionThrown()
{
// Arrange
_mockProfileService.Setup(s => s.GetUserProfileAsync(_userId))
.ThrowsAsync(new Exception("Unexpected error"));
// Act
var result = await _controller.DownloadProfile();
// Assert
var objectResult = result as ObjectResult;
Assert.That(objectResult, Is.Not.Null);
Assert.That(objectResult!.StatusCode, Is.EqualTo(500));
Assert.That(objectResult.Value, Is.EqualTo("Unexpected Error! Please try again later."));
}
}
@@ -0,0 +1,124 @@
using AutoFixture;
using Govor.API.Controllers.Authentication;
using Govor.Application.Interfaces.UserSession;
using Govor.Contracts.Requests;
using Govor.Contracts.Responses;
using Microsoft.AspNetCore.Identity.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.API.Tests.IntegrationTests.Controllers;
[TestFixture]
public class RefreshControllerTests
{
private Fixture _fixture;
private Mock<ILogger<RefreshController>> _mockLogger;
private Mock<IUserSessionRefresher> _mockSessionRefresher;
private RefreshController _controller;
private RefreshTokenRequest _request;
private RefreshResult _result;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_mockLogger = new Mock<ILogger<RefreshController>>();
_mockSessionRefresher = new Mock<IUserSessionRefresher>();
_request = _fixture.Create<RefreshTokenRequest>();
_result = _fixture.Create<RefreshResult>();
_controller = new RefreshController(_mockLogger.Object, _mockSessionRefresher.Object);
}
// Tests for Refresh action
[Test]
public async Task Refresh_ValidRequest_ReturnsOkResult()
{
// Arrange
_mockSessionRefresher.Setup(f => f.RefreshTokenAsync(_request.RefreshToken))
.ReturnsAsync(_result);
// Act
var result = await _controller.Refresh(_request);
//Assert
Assert.That(result, Is.InstanceOf<OkObjectResult>());
var okResult = result as OkObjectResult;
var response = okResult?.Value as RefreshTokenResponse;
Assert.That(response, Is.Not.Null);
Assert.That(response.AccessToken, Is.EqualTo(_result.accessToken));
Assert.That(response.RefreshToken, Is.EqualTo(_result.refreshToken));
}
[Test]
public async Task Refresh_InvalidModelState_ReturnsBadRequest()
{
// Arrange
_controller.ModelState.AddModelError("Error", "Sample error");
// Act
var result = await _controller.Refresh(_request);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
}
[Test]
public async Task Refresh_InvalidRefreshToken_ReturnsBadRequest()
{
// Arrange
_request.RefreshToken = string.Empty;
// Act
var result = await _controller.Refresh(_request);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
var badRequestResult = result as BadRequestObjectResult;
var response = badRequestResult?.Value;
Assert.That(response, Is.EqualTo("Refresh token cant be empty."));
}
[Test]
public async Task Refresh_UnauthorizedAccessException_ReturnsUnauthorizedRequest()
{
// Arrange
_mockSessionRefresher.Setup(f => f.RefreshTokenAsync(_request.RefreshToken))
.ThrowsAsync(new UnauthorizedAccessException());
// Act
var result = await _controller.Refresh(_request);
// Assert
Assert.That(result, Is.InstanceOf<UnauthorizedObjectResult>());
var unauthorized = result as UnauthorizedObjectResult;
var response = unauthorized?.Value;
Assert.That(response, Is.EqualTo("Invalid refresh token"));
}
[Test]
public async Task Refresh_InvalidOperationException_ReturnsBadRequest()
{
// Arrange
_mockSessionRefresher.Setup(f => f.RefreshTokenAsync(_request.RefreshToken))
.ThrowsAsync(new InvalidOperationException());
// Act
var result = await _controller.Refresh(_request);
// Assert
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
}
[TearDown]
public void TearDown()
{
_controller?.Dispose();
}
}
@@ -0,0 +1,55 @@
using AutoFixture;
using Govor.API.Common.SignalR.Helpers;
using Govor.API.Hubs;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Messages;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.API.Tests.IntegrationTests.Hubs;
[TestFixture]
public class ChatsHubTests
{
private Mock<ILogger<ChatsHub>> _loggerMock;
private Mock<IMessageCommandService> _messageServiceMock;
private Mock<IUserGroupsService> _userGroupsServiceMock;
private Mock<IHubUserAccessor> _hubUserAccessorMock;
private Fixture _fixture;
private ChatsHub _chatsHub;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_messageServiceMock = new Mock<IMessageCommandService>();
_userGroupsServiceMock = new Mock<IUserGroupsService>();
_loggerMock = new Mock<ILogger<ChatsHub>>();
_hubUserAccessorMock = new Mock<IHubUserAccessor>();
_chatsHub = new ChatsHub(
_loggerMock.Object,
_messageServiceMock.Object,
_userGroupsServiceMock.Object,
_hubUserAccessorMock.Object
);
}
// Test for Send action
[Test]
public void SendMessage_Success()
{
}
[TearDown]
public void TearDown()
{
_chatsHub?.Dispose();
}
}
@@ -0,0 +1,344 @@
using AutoFixture;
using AutoMapper;
using Govor.API.Common.SignalR.Helpers;
using Govor.API.Hubs;
using Govor.Application.Exceptions.FriendsService;
using Govor.Application.Interfaces.Friends;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Contracts.DTOs;
using Govor.Contracts.Responses.SignalR;
using Govor.Core.Models;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.API.Tests.IntegrationTests.Hubs;
[TestFixture]
public class FriendsHubTests
{
private Mock<IFriendRequestCommandService> _friendRequestServiceMock = null!;
private Mock<IHubUserAccessor> _currentUserServiceMock = null!;
private Mock<IHubCallerClients> _clientsMock = null!;
private Mock<IClientProxy> _clientProxyMock = null!;
private Mock<ILogger<FriendsHub>> _loggerMock = null!;
private Mock<IMapper> _mapperMock = null!;
private FriendsHub _hub = null!;
private Fixture _fixture;
private readonly Guid _userId = Guid.NewGuid();
private readonly Guid _targetUserId = Guid.NewGuid();
[SetUp]
public void Setup()
{
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_friendRequestServiceMock = new Mock<IFriendRequestCommandService>();
_currentUserServiceMock = new Mock<IHubUserAccessor>();
_clientsMock = new Mock<IHubCallerClients>();
_clientProxyMock = new Mock<IClientProxy>();
_loggerMock = new Mock<ILogger<FriendsHub>>();
_mapperMock = new Mock<IMapper>();
_currentUserServiceMock.Setup(x => x.GetUserId(
It.IsAny<HubCallerContext>(),
It.IsAny<bool>()))
.Returns(_userId);
_clientsMock.Setup(c => c.Group(It.IsAny<string>())).Returns(_clientProxyMock.Object);
_hub = new FriendsHub(
_loggerMock.Object,
_friendRequestServiceMock.Object,
_currentUserServiceMock.Object,
_mapperMock.Object)
{
Clients = _clientsMock.Object
};
}
// Tests for SendRequest action
[Test]
public async Task SendRequest_ShouldReturnCreated_WhenSuccess()
{
var friendship = _fixture.Build<Friendship>()
.With(f => f.Status, FriendshipStatus.Pending)
.With(f => f.RequesterId, _userId)
.With(f =>f.AddresseeId, _targetUserId)
.Create();
var dto = new FriendshipDto()
{
Id = friendship.Id,
Status = FriendshipStatus.Accepted,
AddresseeId = friendship.AddresseeId,
RequesterId = friendship.RequesterId,
};
_mapperMock.Setup(f => f.Map<FriendshipDto>(friendship))
.Returns(dto);
_friendRequestServiceMock.Setup(f => f.SendAsync(_userId, _targetUserId))
.ReturnsAsync(friendship);
// Act
var result = await _hub.SendRequest(_targetUserId);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Status, Is.EqualTo(HubResultStatus.Created));
_friendRequestServiceMock.Verify(s => s.SendAsync(_userId, _targetUserId), Times.Once);
_clientProxyMock.Verify(c => c.SendCoreAsync(
"FriendRequestReceived",
It.Is<object[]>(o => o[0]!.Equals(dto)),
default), Times.Once);
}
[Test]
public async Task SendRequest_ShouldReturnBadRequest_WhenInvalidOperation()
{
// Arrange
_friendRequestServiceMock
.Setup(s => s.SendAsync(_userId, _targetUserId))
.ThrowsAsync(new InvalidOperationException("Invalid"));
// Act
var result = await _hub.SendRequest(_targetUserId);
// Assert
Assert.That(result.Status, Is.EqualTo(HubResultStatus.BadRequest));
Assert.That(result.ErrorMessage, Is.EqualTo("Invalid"));
}
[Test]
public async Task SendRequest_ShouldReturnConflict_WhenAlreadySent()
{
// Arrange
_friendRequestServiceMock
.Setup(s => s.SendAsync(_userId, _targetUserId))
.ThrowsAsync(new RequestAlreadySentException(_userId, _targetUserId));
// Act
var result = await _hub.SendRequest(_targetUserId);
// Assert
Assert.That(result.Status, Is.EqualTo(HubResultStatus.Conflict));
Assert.That(result.ErrorMessage, Is.EqualTo($"Request was already sent from {_userId} to {_targetUserId}"));
}
[Test]
public async Task SendRequest_ShouldReturnUnauthorized_WhenCurrentUserIsNotAuthenticated()
{
// Arrange
_currentUserServiceMock.Setup(x => x.GetUserId(It.IsAny<HubCallerContext>(), It.IsAny<bool>()))
.Throws(new UnauthorizedAccessException("userId claim is missing or invalid"));
// Act
var result = await _hub.SendRequest(_targetUserId);
// Assert
Assert.That(result.Status, Is.EqualTo(HubResultStatus.Unauthorized));
Assert.That(result.ErrorMessage, Is.EqualTo("userId claim is missing or invalid"));
}
[Test]
public async Task SendRequest_ShouldReturnError_WhenSomeExceptionOccurs()
{
// Arrange
_friendRequestServiceMock
.Setup(s => s.SendAsync(_userId, _targetUserId))
.ThrowsAsync(new Exception("error"));
// Act
var result = await _hub.SendRequest(_targetUserId);
// Assert
Assert.That(result.Status, Is.EqualTo(HubResultStatus.ServerError));
Assert.That(result.ErrorMessage, Is.EqualTo("Unexpected error! Please try later!"));
}
// Test for AcceptRequest action
[Test]
public async Task AcceptRequest_ShouldReturnOk_WhenSuccess()
{
// Arrange
var friendship = _fixture.Build<Friendship>()
.With(f => f.Status, FriendshipStatus.Pending)
.Create();
var dto = new FriendshipDto()
{
Id = friendship.Id,
Status = FriendshipStatus.Accepted,
AddresseeId = friendship.AddresseeId,
RequesterId = friendship.RequesterId
};
_mapperMock.Setup(f => f.Map<FriendshipDto>(friendship))
.Returns(dto);
_friendRequestServiceMock.Setup(f => f.AcceptAsync(friendship.Id, _userId))
.ReturnsAsync(friendship);
// Act
var result = await _hub.AcceptRequest(friendship.Id);
// Assert
Assert.That(result.Status, Is.EqualTo(HubResultStatus.Success));
_friendRequestServiceMock.Verify(s => s.AcceptAsync(friendship.Id, _userId), Times.Once);
_clientProxyMock.Verify(c => c.SendCoreAsync(
"FriendRequestAccepted",
It.Is<object[]>(o => o.Length == 1 && o[0] == dto),
default), Times.Once);
}
[Test]
public async Task AcceptRequest_ShouldReturnBadRequest_InvalidOperation()
{
// Arrange
var friendshipId = Guid.NewGuid();
_friendRequestServiceMock
.Setup(s => s.AcceptAsync(friendshipId, _userId))
.ThrowsAsync(new InvalidOperationException("Invalid"));
// Act
var result = await _hub.AcceptRequest(friendshipId);
// Assert
Assert.That(result.Status, Is.EqualTo(HubResultStatus.BadRequest));
Assert.That(result.ErrorMessage, Is.EqualTo("Invalid"));
}
[Test]
public async Task AcceptRequest_ShouldReturnUnauthorized_UnauthorizedAccess()
{
// Arrange
var friendshipId = Guid.NewGuid();
_friendRequestServiceMock
.Setup(s => s.AcceptAsync(friendshipId, _userId))
.ThrowsAsync(new UnauthorizedAccessException("userId claim is missing or invalid"));
// Act
var result = await _hub.AcceptRequest(friendshipId);
// Assert
Assert.That(result.Status, Is.EqualTo(HubResultStatus.Unauthorized));
Assert.That(result.ErrorMessage, Is.EqualTo("userId claim is missing or invalid"));
}
[Test]
public async Task AcceptRequest_ShouldReturnError_WhenSomeExceptionOccurs()
{
// Arrange
var friendshipId = Guid.NewGuid();
_friendRequestServiceMock
.Setup(s => s.AcceptAsync(friendshipId, _userId))
.ThrowsAsync(new Exception("error"));
// Act
var result = await _hub.AcceptRequest(friendshipId);
// Assert
Assert.That(result.Status, Is.EqualTo(HubResultStatus.ServerError));
Assert.That(result.ErrorMessage, Is.EqualTo("Unexpected error! Please try later!"));
}
// Test for RejectRequest action
[Test]
public async Task RejectRequest_ShouldReturnOk_WhenSuccess()
{
// Arrange
var friendship = _fixture.Build<Friendship>()
.With(f => f.Status, FriendshipStatus.Pending)
.Create();
var dto = new FriendshipDto()
{
Id = friendship.Id,
Status = FriendshipStatus.Accepted,
AddresseeId = friendship.AddresseeId,
RequesterId = friendship.RequesterId
};
_mapperMock.Setup(f => f.Map<FriendshipDto>(friendship))
.Returns(dto);
_friendRequestServiceMock.Setup(f => f.RejectAsync(friendship.Id, _userId))
.ReturnsAsync(friendship);
// Act
var result = await _hub.RejectRequest(friendship.Id);
// Assert
Assert.That(result.Status, Is.EqualTo(HubResultStatus.Success));
_friendRequestServiceMock.Verify(s => s.RejectAsync(friendship.Id, _userId), Times.Once);
_clientProxyMock.Verify(c => c.SendCoreAsync(
"FriendRequestRejected",
It.Is<object[]>(o => o[0]!.Equals(dto)),
default), Times.Once);
}
[Test]
public async Task RejectRequest_ShouldReturnBadRequest_InvalidOperation()
{
// Arrange
var friendshipId = Guid.NewGuid();
_friendRequestServiceMock
.Setup(s => s.RejectAsync(friendshipId, _userId))
.ThrowsAsync(new InvalidOperationException("Invalid"));
// Act
var result = await _hub.RejectRequest(friendshipId);
// Assert
Assert.That(result.Status, Is.EqualTo(HubResultStatus.BadRequest));
Assert.That(result.ErrorMessage, Is.EqualTo("Invalid"));
}
[Test]
public async Task RejectRequest_ShouldReturnUnauthorized_UnauthorizedAccess()
{
// Arrange
var friendshipId = Guid.NewGuid();
_friendRequestServiceMock
.Setup(s => s.RejectAsync(friendshipId, _userId))
.ThrowsAsync(new UnauthorizedAccessException("userId claim is missing or invalid"));
// Act
var result = await _hub.RejectRequest(friendshipId);
// Assert
Assert.That(result.Status, Is.EqualTo(HubResultStatus.Unauthorized));
Assert.That(result.ErrorMessage, Is.EqualTo("userId claim is missing or invalid"));
}
[Test]
public async Task RejectRequest_ShouldReturnError_WhenSomeExceptionOccurs()
{
// Arrange
var friendshipId = Guid.NewGuid();
_friendRequestServiceMock
.Setup(s => s.RejectAsync(friendshipId, _userId))
.ThrowsAsync(new Exception("error"));
// Act
var result = await _hub.RejectRequest(friendshipId);
// Assert
Assert.That(result.Status, Is.EqualTo(HubResultStatus.ServerError));
Assert.That(result.ErrorMessage, Is.EqualTo("Unexpected error! Please try later!"));
}
[TearDown]
public void TearDown()
{
_hub?.Dispose();
}
}
@@ -4,7 +4,7 @@ using Govor.Application.Services;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Moq; using Moq;
namespace Govor.API.Tests.UnitTests.Services; namespace Govor.Application.Tests.Services;
[TestFixture] [TestFixture]
public class LocalStorageServiceTests public class LocalStorageServiceTests
@@ -0,0 +1,118 @@
using AutoFixture;
using Govor.Core.Infrastructure.Validators;
using Govor.Core.Models;
namespace Govor.API.Tests.UnitTests.Infrastructure.Validators;
[TestFixture]
public class ChatGroupValidatorTests
{
private IObjectValidator<ChatGroup> _validator = new ChatGroupValidator();
private Fixture _fixture;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors
.OfType<ThrowingRecursionBehavior>()
.ToList()
.ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
}
[Test]
public void Validate_ChatGroup_Valid()
{
// Arrange
var chat = _fixture.Create<ChatGroup>();
// Act & Assert
Assert.DoesNotThrow(() => _validator.Validate(chat));
Assert.That(_validator.TryValidate(chat), Is.True);
}
[Test]
public void Validate_ChatGroupIsNull_Invalid()
{
// Act & Assert
Assert.Throws<InvalidObjectException<ChatGroup>>(() => _validator.Validate(default));
Assert.That(_validator.TryValidate(default), Is.False);
}
[Test]
public void Validate_ChatGroupIdIsEmpty_Invalid()
{
// Arrange
var chat = _fixture.Create<ChatGroup>();
chat.Id = Guid.Empty;
// Act & Assert
Assert.Throws<InvalidObjectException<ChatGroup>>(() => _validator.Validate(chat));
Assert.That(_validator.TryValidate(chat), Is.False);
}
[Test]
public void Validate_ChatGroupNameIsEmpty_Invalid()
{
// Arrange
var chat = _fixture.Create<ChatGroup>();
chat.Name = "";
// Act & Assert
Assert.Throws<InvalidObjectException<ChatGroup>>(() => _validator.Validate(chat));
Assert.That(_validator.TryValidate(chat), Is.False);
}
[Test]
public void Validate_ChatGroupDescriptionIsNull_Invalid()
{
// Arrange
var chat = _fixture.Create<ChatGroup>();
chat.Description = default;
// Act & Assert
Assert.Throws<InvalidObjectException<ChatGroup>>(() => _validator.Validate(chat));
Assert.That(_validator.TryValidate(chat), Is.False);
}
[Test]
public void Validate_ChatGroupImageIdIsEmpty_Invalid()
{
// Arrange
var chat = _fixture.Create<ChatGroup>();
chat.ImageId = Guid.Empty;
// Act & Assert
Assert.Throws<InvalidObjectException<ChatGroup>>(() => _validator.Validate(chat));
Assert.That(_validator.TryValidate(chat), Is.False);
}
[Test]
public void Validate_ChatGroupIsPrivateAndInvitesCodesIsEmpty_Invalid()
{
// Arrange
var chat = _fixture.Create<ChatGroup>();
chat.IsPrivate = true;
chat.InviteCodes = new();
// Act & Assert
Assert.Throws<InvalidObjectException<ChatGroup>>(() => _validator.Validate(chat));
Assert.That(_validator.TryValidate(chat), Is.False);
}
[Test]
public void Validate_ChatGroupAdminsIsEmpty_Invalid()
{
// Arrange
var chat = _fixture.Create<ChatGroup>();
chat.Admins = new();
// Act & Assert
Assert.Throws<InvalidObjectException<ChatGroup>>(() => _validator.Validate(chat));
Assert.That(_validator.TryValidate(chat), Is.False);
}
}
@@ -1,6 +1,7 @@
using AutoFixture; using AutoFixture;
using Govor.Core.Infrastructure.Validators; using Govor.Core.Infrastructure.Validators;
using Govor.Core.Models; using Govor.Core.Models;
using Govor.Core.Models.Messages;
namespace Govor.API.Tests.UnitTests.Infrastructure.Validators; namespace Govor.API.Tests.UnitTests.Infrastructure.Validators;
@@ -0,0 +1,77 @@
using AutoFixture;
using Govor.Core.Infrastructure.Validators;
using Govor.Core.Models;
namespace Govor.API.Tests.UnitTests.Infrastructure.Validators;
[TestFixture]
public class PrivateChatValidatorTests
{
private IObjectValidator<PrivateChat> _validator = new PrivateChatValidator();
private Fixture _fixture;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors
.OfType<ThrowingRecursionBehavior>()
.ToList()
.ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
}
[Test]
public void Validate_PrivateChat_Valid()
{
// Arrange
var privateChat = _fixture.Create<PrivateChat>();
// Act & Assert
Assert.DoesNotThrow(() => _validator.Validate(privateChat));
Assert.That(_validator.TryValidate(privateChat), Is.True);
}
[Test]
public void Validate_When_PrivateChatIsNull_Invalid()
{
// Act & Assert
Assert.Throws<InvalidObjectException<PrivateChat>>(() => _validator.Validate(default));
Assert.That(_validator.TryValidate(default), Is.False);
}
[Test]
public void Validate_When_PrivateChatIdIsEmpty_Invalid()
{
// Arrange
var privateChat = _fixture.Create<PrivateChat>();
privateChat.Id = Guid.Empty;
// Act & Assert
Assert.Throws<InvalidObjectException<PrivateChat>>(() => _validator.Validate(privateChat));
Assert.That(_validator.TryValidate(privateChat), Is.False);
}
[Test]
public void Validate_When_UserAIdIsEmpty_Invalid()
{
// Arrange
var privateChat = _fixture.Create<PrivateChat>();
privateChat.UserAId = Guid.Empty;
// Act & Assert
Assert.Throws<InvalidObjectException<PrivateChat>>(() => _validator.Validate(privateChat));
Assert.That(_validator.TryValidate(privateChat), Is.False);
}
[Test]
public void Validate_When_UserBIdIsEmpty_Invalid()
{
// Arrange
var privateChat = _fixture.Create<PrivateChat>();
privateChat.UserBId = Guid.Empty;
// Act & Assert
Assert.Throws<InvalidObjectException<PrivateChat>>(() => _validator.Validate(privateChat));
Assert.That(_validator.TryValidate(privateChat), Is.False);
}
}
@@ -1,6 +1,7 @@
using AutoFixture; using AutoFixture;
using Govor.Core.Infrastructure.Validators; using Govor.Core.Infrastructure.Validators;
using Govor.Core.Models; using Govor.Core.Models;
using Govor.Core.Models.Users;
namespace Govor.API.Tests.UnitTests.Infrastructure.Validators; namespace Govor.API.Tests.UnitTests.Infrastructure.Validators;
@@ -1,59 +0,0 @@
using System.IdentityModel.Tokens.Jwt;
using AutoFixture;
using Govor.API.Services.Authentication.Interfaces;
using Govor.Application.Services;
using Govor.Core.Models;
using Microsoft.Extensions.Options;
using Moq;
namespace Govor.API.Tests.UnitTests.Services.Authentication;
[TestFixture]
public class JwtServiceTests
{
private Fixture _fixture;
private Mock<IOptions<JwtOption>> _jwtOptionsMock;
private Mock<IInvitesService> _invitesServiceMock;
private IJwtService _jwtService;
private JwtOption _testJwtOptions;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_testJwtOptions = new JwtOption
{
SecretKeу = "THIS IS A TEST SECRET KEY THAT IS LONG ENOUGH", // Ensure key size is sufficient for HMACSHA256
Hours = 1
};
_jwtOptionsMock = new Mock<IOptions<JwtOption>>();
_jwtOptionsMock.Setup(o => o.Value).Returns(_testJwtOptions);
_invitesServiceMock = new Mock<IInvitesService>();
_jwtService = new JwtService(_jwtOptionsMock.Object, _invitesServiceMock.Object);
}
[Test]
public void GenerateJwtToken_ShouldReturnValidJwtString()
{
// Arrange
var user = _fixture.Create<User>();
var expectedRole = "User";
_invitesServiceMock.Setup(s => s.GetRole(user)).Returns(Task.FromResult(expectedRole));
// Act
var tokenString = _jwtService.GenerateJwtToken(user);
// Assert
Assert.That(tokenString, Is.Not.Null.And.Not.Empty);
// Attempt to parse the token to ensure it's a JWT
var handler = new JwtSecurityTokenHandler();
Assert.DoesNotThrow(() => handler.ReadJwtToken(tokenString));
}
}
@@ -1,358 +0,0 @@
using AutoFixture;
using Govor.Application.Exceptions.FriendsService;
using Govor.Application.Interfaces;
using Govor.Application.Services;
using Govor.Core.Models;
using Govor.Core.Repositories.Friendships;
using Govor.Core.Repositories.Users;
using Govor.Data.Repositories.Exceptions;
using Moq;
namespace Govor.API.Tests.UnitTests.Services;
[TestFixture]
public class FriendsServiceTests
{
private Fixture _fixture;
private Mock<IUsersRepository> _usersRepositoryMock;
private Mock<IFriendshipsRepository> _friendshipsRepositoryMock;
private IFriendsService _service;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors
.OfType<ThrowingRecursionBehavior>()
.ToList()
.ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_usersRepositoryMock = new Mock<IUsersRepository>();
_friendshipsRepositoryMock = new Mock<IFriendshipsRepository>();
_service = new FriendsService(_usersRepositoryMock.Object, _friendshipsRepositoryMock.Object);
}
// SearchUsersAsync
[Test]
public async Task SearchUsersAsync_ReturnsUsers_IfNotAlreadyFriends()
{
// Arrange
var userId = _fixture.Create<Guid>();
var user = _fixture.Create<User>();
user.Id = Guid.NewGuid();
_usersRepositoryMock
.Setup(u => u.SearchPotentialFriendsAsync(userId, It.IsAny<string>()))
.ReturnsAsync(new List<User> { user });
_friendshipsRepositoryMock
.Setup(f => f.FindByUserIdAsync(userId))
.ReturnsAsync(new List<Friendship>()); // No friends
// Act
var result = await _service.SearchUsersAsync("test", userId);
// Assert
Assert.That(result, Has.Count.EqualTo(1));
Assert.That(result[0].Id, Is.EqualTo(user.Id));
}
[Test]
public void SearchUsersAsync_Throws_SearchUsersException_IfThrowsNotFoundByKeyException_String_Guid()
{
// Arrange
_usersRepositoryMock
.Setup(u => u.SearchPotentialFriendsAsync(It.IsAny<Guid>(), It.IsAny<string>()))
.ThrowsAsync(new NotFoundByKeyException<(string,Guid)>(("test",Guid.NewGuid()),"test"));
// Axt & Assert
Assert.ThrowsAsync<SearchUsersException>(async () => await _service.SearchUsersAsync("test", Guid.NewGuid()));
}
[Test]
public async Task SearchUsersAsync_ReturnsUsersWithoutFriendships_IfThrowsNotFoundByKeyException_Guid()
{
// Arrange
var userId = _fixture.Create<Guid>();
var user = _fixture.Create<User>();
user.Id = Guid.NewGuid();
_usersRepositoryMock
.Setup(u => u.SearchPotentialFriendsAsync(userId, It.IsAny<string>()))
.ReturnsAsync(new List<User> { user });
_friendshipsRepositoryMock
.Setup(f => f.FindByUserIdAsync(userId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(userId, "test"));
// Act
var result = await _service.SearchUsersAsync("test", userId);
// Assert
Assert.That(result, Has.Count.EqualTo(1));
Assert.That(result[0].Id, Is.EqualTo(user.Id));
}
[Test]
public void SearchUsersAsync_Throws_UnauthorizedAccessException_IfThrowsSomeExceptionInUsersRepository()
{
// Arrange
_usersRepositoryMock
.Setup(u => u.SearchPotentialFriendsAsync(It.IsAny<Guid>(), It.IsAny<string>()))
.ThrowsAsync(new Exception("test"));
// Act & Assert
Assert.ThrowsAsync<UnauthorizedAccessException>(async () => await _service.SearchUsersAsync("test", Guid.NewGuid()));
}
[Test]
public void SearchUsersAsync_Throws_UnauthorizedAccessException_IfThrowsSomeExceptionInFriendshipsRepository()
{
// Arrange
_friendshipsRepositoryMock
.Setup(u => u.FindByUserIdAsync(It.IsAny<Guid>()))
.ThrowsAsync(new Exception("test"));
// Act & Assert
Assert.ThrowsAsync<UnauthorizedAccessException>(async () => await _service.SearchUsersAsync("test", Guid.NewGuid()));
}
// SendFriendRequestAsync
[Test]
public void SendFriendRequestAsync_SendingToSelf_ThrowsInvalidOperationException()
{
// Arrange
var userId = Guid.NewGuid();
// Act & Assert
var ex = Assert.ThrowsAsync<InvalidOperationException>(() =>
_service.SendFriendRequestAsync(userId, userId));
Assert.That(ex.Message, Is.EqualTo("Cannot send a request to self user"));
}
[Test]
public void SendFriendRequestAsync_RequestAlreadyExists_ThrowsRequestAlreadySentException()
{
// Arrange
var fromUserId = Guid.NewGuid();
var toUserId = Guid.NewGuid();
_friendshipsRepositoryMock
.Setup(repo => repo.Exist(fromUserId, toUserId))
.Returns(true);
// Act & Assert
Assert.ThrowsAsync<RequestAlreadySentException>(() =>
_service.SendFriendRequestAsync(fromUserId, toUserId));
}
[Test]
public async Task SendFriendRequestAsync_ValidRequest_CallsAddAsync()
{
// Arrange
var fromUserId = Guid.NewGuid();
var toUserId = Guid.NewGuid();
_friendshipsRepositoryMock
.Setup(repo => repo.Exist(fromUserId, toUserId))
.Returns(false);
// Act
await _service.SendFriendRequestAsync(fromUserId, toUserId);
// Assert
_friendshipsRepositoryMock.Verify(repo =>
repo.AddAsync(It.Is<Friendship>(f =>
f.RequesterId == fromUserId &&
f.AddresseeId == toUserId &&
f.Status == FriendshipStatus.Pending &&
f.Id != Guid.Empty
)),
Times.Once);
}
// AcceptFriendRequestAsync
[Test]
public async Task AcceptFriendRequestAsync_ValidRequest_CallsUpdateAsync()
{
var friendship = _fixture.Build<Friendship>()
.With(f => f.Status, FriendshipStatus.Pending)
.Create();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(friendship.Id))
.ReturnsAsync(friendship);
// Act: вызываем именно Accept, не Send
await _service.AcceptFriendRequestAsync(friendship.Id, friendship.AddresseeId);
// Assert
_friendshipsRepositoryMock.Verify(r => r.UpdateAsync(It.Is<Friendship>(f =>
f.Id == friendship.Id &&
f.RequesterId == friendship.RequesterId &&
f.AddresseeId == friendship.AddresseeId &&
f.Status == FriendshipStatus.Accepted
)), Times.Once);
}
[Test]
public void AcceptFriendRequestAsync_Throws_InvalidOperationException_IfStatusNotEqualPending()
{
var friendship = _fixture.Build<Friendship>()
.With(f => f.Status, FriendshipStatus.Accepted)
.Create();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(friendship.Id))
.ReturnsAsync(friendship);
// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(async () => await _service.AcceptFriendRequestAsync(friendship.Id, friendship.AddresseeId));
}
[Test]
public void AcceptFriendRequestAsync_Throws_UnauthorizedAccessException_IfCurrentUserIdIsNotAddressee()
{
// Arrange
var friendship = _fixture.Build<Friendship>()
.With(f => f.Status, FriendshipStatus.Pending)
.Create();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(friendship.Id))
.ReturnsAsync(friendship);
var wrongUserId = Guid.NewGuid(); // не совпадает с AddresseeId
// Act & Assert
Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
await _service.AcceptFriendRequestAsync(friendship.Id, wrongUserId));
}
[Test]
public void AcceptFriendRequestAsync_Throws_InvalidOperationException_IfFriendshipNotFound()
{
// Arrange
var requestId = Guid.NewGuid();
var userId = Guid.NewGuid();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(requestId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(requestId));
// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await _service.AcceptFriendRequestAsync(requestId, userId));
}
[Test]
public async Task AcceptFriendRequestAsync_ChangesStatusToAccepted()
{
var friendship = _fixture.Build<Friendship>()
.With(f => f.Status, FriendshipStatus.Pending)
.Create();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(friendship.Id))
.ReturnsAsync(friendship);
Friendship updatedFriendship = null;
_friendshipsRepositoryMock
.Setup(r => r.UpdateAsync(It.IsAny<Friendship>()))
.Callback<Friendship>(f => updatedFriendship = f)
.Returns(Task.CompletedTask);
await _service.AcceptFriendRequestAsync(friendship.Id, friendship.AddresseeId);
Assert.That(updatedFriendship.Status, Is.EqualTo(FriendshipStatus.Accepted));
}
// GetFriendsAsync
[Test]
public async Task GetFriendsAsync_ReturnsUsers_IfUserExists()
{
// Arrange
var userId = Guid.NewGuid();
var friendships = _fixture.CreateMany<Friendship>().ToList();
friendships.ForEach(f =>
{
f.RequesterId = userId;
f.Status = FriendshipStatus.Accepted;
});
_friendshipsRepositoryMock.Setup(f => f.FindByUserIdAsync(userId))
.ReturnsAsync(friendships);
// Act
var result = await _service.GetFriendsAsync(userId);
// Assert
Assert.That(result.Count, Is.EqualTo(friendships.Count));
Assert.That(result, Is.EquivalentTo(friendships.Select(f => f.Addressee)));
}
[Test]
public void GetFriendsAsync_Throws_InvalidOperationException_IfUserNotExists()
{
// Arrange
_friendshipsRepositoryMock.Setup(f => f.FindByUserIdAsync(It.IsAny<Guid>()))
.ThrowsAsync(new NotFoundByKeyException<Guid>(Guid.NewGuid()));
// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(async () => await _service.GetFriendsAsync(Guid.NewGuid()));
}
// GetIncomingRequestsAsync
[Test]
public async Task GetIncomingRequestsAsync_ReturnsFriendships_IfFriendshipsExists()
{
// Arrange
var userId = Guid.NewGuid();
var friendships = _fixture.CreateMany<Friendship>().ToList();
var user = _fixture.Build<User>()
.With(u => u.Id, userId)
.With(u => u.ReceivedFriendRequests, friendships)
.Create();
friendships.ForEach(f =>
{
f.AddresseeId = userId;
f.Addressee = user;
f.Status = FriendshipStatus.Pending;
});
_usersRepositoryMock.Setup(f => f.FindByIdAsync(userId))
.ReturnsAsync(user);
// Act
var result = await _service.GetIncomingRequestsAsync(userId);
// Assert
Assert.That(result.Count, Is.EqualTo(friendships.Count));
Assert.That(result.Select(u => u.Id), Is.EquivalentTo(friendships.Select(f => f.Id)));
}
[Test]
public void GetIncomingRequestsAsync_ThrowsInvalidOperationException_WhenUserNotFound()
{
// Arrange
var userId = Guid.NewGuid();
_usersRepositoryMock
.Setup(r => r.FindByIdAsync(userId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(userId));
// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await _service.GetIncomingRequestsAsync(userId));
}
}
@@ -0,0 +1,14 @@
using Govor.Application.Services.Authentication;
namespace Govor.API.Common.Extensions;
public static class AddOptionExtensions
{
public static IServiceCollection AddOptionsConfiguration(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<JwtAccessOption>(configuration.GetSection(nameof(JwtAccessOption)));
services.Configure<JwtRefreshOption>(configuration.GetSection(nameof(JwtRefreshOption)));
return services;
}
}
@@ -0,0 +1,158 @@
using Govor.API.Common.Mapping;
using Govor.API.Common.SignalR.Helpers;
using Govor.Application.Infrastructure.AdminsStuff;
using Govor.Application.Infrastructure.Extensions;
using Govor.Application.Infrastructure.Validators;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Authentication;
using Govor.Application.Interfaces.Friends;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Application.Interfaces.Medias;
using Govor.Application.Interfaces.Messages;
using Govor.Application.Interfaces.UserOnlineStatus;
using Govor.Application.Interfaces.UserSession;
using Govor.Application.Interfaces.UserSession.Crypto;
using Govor.Application.Services;
using Govor.Application.Services.Authentication;
using Govor.Application.Services.Friends;
using Govor.Application.Services.Medias;
using Govor.Application.Services.Messages;
using Govor.Application.Services.UserOnlineStatus;
using Govor.Application.Services.UserSessions;
using Govor.Application.Services.UserSessions.Crypto;
using Govor.Core.Infrastructure.Extensions;
using Govor.Core.Infrastructure.Validators;
using Govor.Core.Models;
using Govor.Core.Models.Messages;
using Govor.Core.Models.Users;
using Govor.Core.Repositories.Admins;
using Govor.Core.Repositories.Friendships;
using Govor.Core.Repositories.Groups;
using Govor.Core.Repositories.Invaites;
using Govor.Core.Repositories.MediasAttachments;
using Govor.Core.Repositories.Messages;
using Govor.Core.Repositories.PrivateChats;
using Govor.Core.Repositories.Users;
using Govor.Core.Repositories.UserSessionsRepository;
using Govor.Data;
using Govor.Data.Repositories;
using Microsoft.EntityFrameworkCore;
namespace Govor.API.Common.Extensions;
public static class ConfigurationProgramExtensions
{
public static void AddServices(this IServiceCollection services)
{
services.AddScoped<IPasswordHasher, PasswordHasher>();
services.AddScoped<IJwtTokenHasher, JwtTokenHasher>();
services.AddScoped<IJwtService, JwtService>();
services.AddScoped<IAccountService, AuthService>();
services.AddScoped<IUsersAdministration, UsersService>();
services.AddScoped<IInvitesService, InvitesService>();
services.AddScoped<IInvitationGenerator, InvitationGenerator>();
services.AddScoped<IUsernameValidator, UsernameValidator>();
// Friends services
services.AddScoped<IFriendshipService, FriendshipService>();
services.AddScoped<IFriendRequestCommandService, FriendRequestCommandService>();
services.AddScoped<IFriendRequestQueryService, FriendRequestQueryService>();
services.AddScoped<IFriendsBlockService, FriendsBlockService>();
services.AddScoped<IStorageService>(sp =>
{
var env = sp.GetRequiredService<IWebHostEnvironment>();
return new LocalStorageService(env.ContentRootPath);
});
services.AddHttpContextAccessor(); // it's very important for CurrentUserService
services.AddScoped<ICurrentUserService, CurrentUserService>();
services.AddScoped<ICurrentUserSessionService, CurrentUserSessionService>();
services.AddMemoryCache();
services.AddScoped<IPingHandlerService, PingHandlerService>();
services.AddScoped<IMessageCommandService, MessageCommandService>();
services.AddScoped<IVerifyFriendship, VerifyFriendship>();
services.AddScoped<IUserGroupsService, UserGroupsService>();
services.AddScoped<IMessagesLoader, MessagesLoader>();
services.AddScoped<IMediaService, MediaService>();
services.AddScoped<IAccesserToDownloadMedia, AccesserToDownloadMediaService>();
// UserSession
services.AddScoped<IUserSessionOpener, UserSessionOpener>();
services.AddScoped<IUserSessionRefresher, UserSessionRefresher>();
services.AddScoped<IUserNotificationScopeService, UserNotificationScopeService>();
services.AddScoped<IUserPresenceReader, UserPresenceReader>();
services.AddSingleton<IOnlineUserStore, OnlineUserStore>();
// Auto Mapper
services.AddAutoMapper(typeof(MappingProfile));
services.AddScoped<IHubUserAccessor, HubUserAccessor>();
services.AddScoped<IUserSessionReader, UserSessionReader>();
services.AddScoped<IUserSessionRevoker, UserSessionRevoker>();
services.AddScoped<ISessionKeyAttacher, SessionKeyAttacher>();
services.AddScoped<ISessionKeysReader, SessionKeysReader>();
services.AddScoped<IOneTimePreKeysRotator, OneTimePreKeysRotator>();
services.AddScoped<IProfileService, ProfileService>();
}
public static void AddRepositories(this IServiceCollection services)
{
services.AddScoped<IUsersRepository, UsersRepository>();
services.AddScoped<IMessagesRepository, MessagesRepository>();
services.AddScoped<IInvitesRepository, InvitesRepository>();
services.AddScoped<IAdminsRepository, AdminsRepository>();
services.AddScoped<IMediaAttachmentsRepository, MediaAttachmentsRepository>();
services.AddScoped<IFriendshipsRepository, FriendshipsRepository>();
services.AddScoped<IPrivateChatsRepository, PrivateChatsRepository>();
services.AddScoped<IGroupsRepository, GroupRepository>();
services.AddScoped<IUserSessionsRepository, UserSessionsRepository>();
// other
}
public static void AddValidators(this IServiceCollection services)
{
services.AddScoped<IObjectValidator<User>, UserValidator>();
services.AddScoped<IObjectValidator<Message>, MessageValidator>();
services.AddScoped<IObjectValidator<MediaAttachments>, MediaAttachmentsValidator>();
services.AddScoped<IObjectValidator<Admin>, AdminValidator>();
services.AddScoped<IObjectValidator<Invitation>, InvitationValidator>();
services.AddScoped<IObjectValidator<Friendship>, FriendshipValidator>();
services.AddScoped<IObjectValidator<PrivateChat>, PrivateChatValidator>();
services.AddScoped<IObjectValidator<ChatGroup>, ChatGroupValidator>();
}
public static void AddGovorDbContext(this IServiceCollection services, IConfiguration configuration)
{
var useMySql = configuration.GetValue<bool>("UseMySql");
if (useMySql)
{
services.AddDbContext<GovorDbContext>(options =>
{
options
.UseMySql(
configuration.GetConnectionString(nameof(GovorDbContext)),
new MySqlServerVersion(new Version(8, 0, 21))
);
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});
}
else
{
services.AddDbContext<GovorDbContext>(options =>
{
options.UseNpgsql(configuration.GetConnectionString(nameof(GovorDbContext)));
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});
}
}
}
@@ -0,0 +1,15 @@
using Govor.API.Filters;
using Microsoft.AspNetCore.SignalR;
namespace Govor.API.Common.Extensions;
public static class ConfigurationSignalR
{
public static void AddSignalRConf(this IServiceCollection services)
{
services.AddSignalR(options =>
{
options.AddFilter<HubExceptionFilter>();
});
}
}
@@ -0,0 +1,17 @@
using Serilog;
namespace Govor.API.Common.Extensions;
public static class ConfiguratorLoggerExtensions
{
public static void AddLogger(this WebApplicationBuilder builder)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console()
.WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day) // Лог в файл, ежедневно
.CreateLogger();
builder.Host.UseSerilog();
}
}
@@ -0,0 +1,30 @@
using AutoMapper;
using Govor.API.Extensions.Mapping;
using Govor.Application.Profiles;
using Govor.Contracts.DTOs;
using Govor.Contracts.Responses;
using Govor.Core.Models;
using Govor.Core.Models.Messages;
using Govor.Core.Models.Users;
namespace Govor.API.Common.Mapping;
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Message, MessageResponse>();
CreateMap<MediaAttachments, MediaAttachmentResponse>();
CreateMap<MessageReaction, MessageReactionResponse>();
CreateMap<MessageView, MessageViewResponse>();
CreateMap<User, UserDto>()
.AfterMap<UserToUserDtoMappingAction>();
CreateMap<Friendship, FriendshipDto>();
CreateMap<UserSession, SessionDto>();
CreateMap<UserProfile, UserProfileDto>();
}
}
@@ -0,0 +1,21 @@
using AutoMapper;
using Govor.Application.Interfaces.UserOnlineStatus;
using Govor.Contracts.DTOs;
using Govor.Core.Models.Users;
namespace Govor.API.Extensions.Mapping;
public class UserToUserDtoMappingAction : IMappingAction<User, UserDto>
{
private readonly IOnlineUserStore _onlineUserStore;
public UserToUserDtoMappingAction(IOnlineUserStore onlineUserStore)
{
_onlineUserStore = onlineUserStore;
}
public void Process(User source, UserDto destination, ResolutionContext context)
{
destination.IsOnline = _onlineUserStore.IsOnline(source.Id);
}
}
@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.SignalR;
namespace Govor.API.Common.SignalR.Helpers;
public class HubUserAccessor : IHubUserAccessor
{
private readonly ILogger<HubUserAccessor> _logger;
public HubUserAccessor(ILogger<HubUserAccessor> logger)
{
_logger = logger;
}
public Guid GetUserId(HubCallerContext context, bool suppressException = false)
{
var userIdClaim = context.User?.FindFirst("userId")?.Value;
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
{
if (!suppressException)
{
_logger.LogError("Could not retrieve sender userId. Claim was: {UserIDClaim}", userIdClaim);
throw new UnauthorizedAccessException("userID claim is missing or invalid.");
}
return Guid.Empty;
}
return userId;
}
}
@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.SignalR;
namespace Govor.API.Common.SignalR.Helpers;
public interface IHubUserAccessor
{
Guid GetUserId(HubCallerContext context, bool suppressException = false);
}
@@ -0,0 +1,92 @@
using Govor.Contracts.DTOs;
using Govor.Core.Models;
using Govor.Core.Models.Users;
using Govor.Core.Repositories.Friendships;
using Govor.Data.Repositories.Exceptions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Govor.API.Controllers.AdminStuff;
[ApiController]
[Route("api/admin/[controller]")]
[Authorize(Roles = "Admin")]
public class FriendshipsController : Controller
{
private readonly ILogger<FriendshipsController> _logger;
private readonly IFriendshipsRepository _friendshipsRepository;
public FriendshipsController(ILogger<FriendshipsController> logger, IFriendshipsRepository friendshipsRepository)
{
_logger = logger;
_friendshipsRepository = friendshipsRepository;
}
[HttpGet]
public async Task<IActionResult> Get()
{
try
{
_logger.LogInformation("Get all friendships by administrator");
var result = await _friendshipsRepository.GetAllAsync();
return Ok(result);
}
catch (NotFoundException ex)
{
_logger.LogWarning(ex.Message);
return NotFound(ex.Message);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
return StatusCode(500, new { error = "Internal server error." });
}
}
[HttpGet("{userId}")]
public async Task<IActionResult> GetByUserId(Guid userId)
{
if(userId == Guid.Empty)
return BadRequest("User id can't be empty.");
try
{
_logger.LogInformation($"Get user's {userId} all friendships by administrator");
var result = await _friendshipsRepository.FindByUserIdAsync(userId);
return Ok(result);
}
catch (NotFoundByKeyException<Guid> ex)
{
_logger.LogWarning(ex.Message);
return NotFound(ex.Message);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
return StatusCode(500, new { error = "Internal server error." });
}
}
[HttpPost]
public async Task<IActionResult> RemoveFriendship(Guid Id)
{
if(Id == Guid.Empty)
return BadRequest("FS id can't be empty.");
try
{
var result = await _friendshipsRepository.GetByIdAsync(Id);
await _friendshipsRepository.RemoveAsync(result);
return Ok();
}
catch (NotFoundByKeyException<Guid> ex)
{
_logger.LogWarning(ex.Message);
return NotFound(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, new { error = "Internal server error." });
}
}
}
@@ -1,4 +1,4 @@
using Govor.API.Services.AdminsStuff.Interfaces; using Govor.Application.Interfaces;
using Govor.Contracts.DTOs; using Govor.Contracts.DTOs;
using Govor.Contracts.Requests; using Govor.Contracts.Requests;
using Govor.Core.Repositories.Invaites; using Govor.Core.Repositories.Invaites;
@@ -7,9 +7,9 @@ using Microsoft.AspNetCore.Mvc;
namespace Govor.API.Controllers.AdminStuff; namespace Govor.API.Controllers.AdminStuff;
[Route("api/[controller]")] [Route("api/admin/[controller]")]
[ApiController] [ApiController]
[Authorize(Roles = "Admin")] //[Authorize(Roles = "Admin")]
public class InviteUserController : Controller public class InviteUserController : Controller
{ {
private readonly IInvitesRepository _repository; private readonly IInvitesRepository _repository;
@@ -52,13 +52,13 @@ public class InviteUserController : Controller
_logger.LogInformation("Getting all active invitations by administrator"); _logger.LogInformation("Getting all active invitations by administrator");
var read = await _repository.GetAllAsync(); var read = await _repository.GetAllAsync();
var result = read.Where(x => x.IsActive == true).ToList(); var result = read.Where(x => x.IsActive).ToList();
List<InvitationDto> dtos = new List<InvitationDto>(); List<InvitationResponses> dtos = new List<InvitationResponses>();
foreach (var inv in result) foreach (var inv in result)
{ {
dtos.Add(new InvitationDto(){ dtos.Add(new InvitationResponses(){
Id = inv.Id, Id = inv.Id,
Description = inv.Description, Description = inv.Description,
IsAdmin = inv.IsAdmin, IsAdmin = inv.IsAdmin,
@@ -67,6 +67,7 @@ public class InviteUserController : Controller
CreatedAt = inv.DateCreated, CreatedAt = inv.DateCreated,
EndAt = inv.EndDate, EndAt = inv.EndDate,
IsActive = inv.IsActive, IsActive = inv.IsActive,
ParticipantCount = inv.Users.Count,
}); });
} }
@@ -87,11 +88,11 @@ public class InviteUserController : Controller
_logger.LogInformation("Getting all invitations by administrator"); _logger.LogInformation("Getting all invitations by administrator");
var read = await _repository.GetAllAsync(); var read = await _repository.GetAllAsync();
List<InvitationDto> dtos = new List<InvitationDto>(); List<InvitationResponses> dtos = new List<InvitationResponses>();
foreach (var inv in read) foreach (var inv in read)
{ {
dtos.Add(new InvitationDto(){ dtos.Add(new InvitationResponses(){
Id = inv.Id, Id = inv.Id,
Description = inv.Description, Description = inv.Description,
IsAdmin = inv.IsAdmin, IsAdmin = inv.IsAdmin,
@@ -100,6 +101,7 @@ public class InviteUserController : Controller
CreatedAt = inv.DateCreated, CreatedAt = inv.DateCreated,
EndAt = inv.EndDate, EndAt = inv.EndDate,
IsActive = inv.IsActive, IsActive = inv.IsActive,
ParticipantCount = inv.Users.Count,
}); });
} }
@@ -120,7 +122,7 @@ public class InviteUserController : Controller
_logger.LogInformation("Getting invitations {id} by administrator"); _logger.LogInformation("Getting invitations {id} by administrator");
var read = await _repository.FindByIdAsync(id); var read = await _repository.FindByIdAsync(id);
var response = new InvitationDto(){ var response = new InvitationResponses(){
Id = read.Id, Id = read.Id,
Description = read.Description, Description = read.Description,
IsAdmin = read.IsAdmin, IsAdmin = read.IsAdmin,
@@ -129,9 +131,9 @@ public class InviteUserController : Controller
CreatedAt = read.DateCreated, CreatedAt = read.DateCreated,
EndAt = read.EndDate, EndAt = read.EndDate,
IsActive = read.IsActive, IsActive = read.IsActive,
ParticipantCount = read.Users.Count,
}; };
return Ok(response); return Ok(response);
} }
catch (Exception e) catch (Exception e)
@@ -1,4 +1,7 @@
using Govor.API.Services.AdminsStuff.Interfaces; using Govor.Application.Interfaces;
using Govor.Contracts.Responses.Admins;
using Govor.Core.Models.Users;
using Govor.Data.Repositories.Exceptions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -21,16 +24,55 @@ public class UsersController : Controller
[HttpGet] [HttpGet]
public async Task<IActionResult> AllUsers() public async Task<IActionResult> AllUsers()
{
try
{ {
_logger.LogInformation("Getting all users by administrator"); _logger.LogInformation("Getting all users by administrator");
var read = await _users.GetAllUsersAsync(); var read = await _users.GetAllUsersAsync();
return Ok(read); return Ok(BuildUserDtos(read));
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
return StatusCode(500, e.Message);
}
} }
[HttpGet("{id:guid}")] [HttpGet("{id:guid}")]
public async Task<IActionResult> GetUserById(Guid id) public async Task<IActionResult> GetUserById(Guid id)
{ {
return Ok(id); try
{
_logger.LogInformation($"Getting user {id} by administrator");
var read = await _users.GetUserById(id);
return Ok(BuildUserDtos([read]).First());
} }
catch (NotFoundByKeyException<Guid> ex)
{
_logger.LogWarning(ex, ex.Message);
return NotFound(ex.Message);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
return StatusCode(500, e.Message);
}
}
private List<UserResponse> BuildUserDtos(IEnumerable<User> users) => users.Select(user => new UserResponse
{
Id = user.Id,
Username = user.Username,
Description = user.Description,
WasOnline = user.WasOnline,
IconId = user.IconId,
PasswordHash = user.PasswordHash,
InviteId = user.InviteId,
CreatedOn = user.CreatedOn,
IsAdmin = user.Invite?.IsAdmin ?? false,
}).ToList();
} }
@@ -1,44 +1,56 @@
using Govor.API.Services.Authentication.Interfaces;
using Govor.Application.Exceptions.AuthService; using Govor.Application.Exceptions.AuthService;
using Govor.Application.Exceptions.InvitesService; using Govor.Application.Exceptions.InvitesService;
using Govor.Application.Interfaces.Authentication; using Govor.Application.Interfaces.Authentication;
using Govor.Application.Interfaces.UserSession;
using Govor.Contracts.Requests; using Govor.Contracts.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Govor.API.Controllers; namespace Govor.API.Controllers.Authentication;
[ApiController] [ApiController]
[AllowAnonymous]
[Route("api/[controller]")] [Route("api/[controller]")]
public class AuthController : Controller public class AuthController : Controller
{ {
private IUserSessionOpener _userSession;
private IInvitesService _invitesService; private IInvitesService _invitesService;
private IAccountService _accountService; private IAccountService _accountService;
private ILogger<AuthController> _logger; private ILogger<AuthController> _logger;
public AuthController(IAccountService accountService, IInvitesService invitesService, ILogger<AuthController> logger) public AuthController(
IAccountService accountService,
IInvitesService invitesService,
IUserSessionOpener userSessionOpener,
ILogger<AuthController> logger)
{ {
_userSession = userSessionOpener;
_accountService = accountService; _accountService = accountService;
_invitesService = invitesService; _invitesService = invitesService;
_logger = logger; _logger = logger;
} }
//[RequireHttps]
[HttpPost("register")]// api/auth/register [HttpPost("register")]// api/auth/register
[RequireHttps]
public async Task<IActionResult> Register([FromBody] RegistrationRequest registrationRequest) public async Task<IActionResult> Register([FromBody] RegistrationRequest registrationRequest)
{ {
try try
{ {
if (!ModelState.IsValid) if (!ModelState.IsValid)
{
return BadRequest(ModelState); return BadRequest(ModelState);
}
var invite = _invitesService.Validate(registrationRequest.InviteLink); var invite = await _invitesService.ValidateAsync(registrationRequest.InviteLink);
var token = await _accountService.RegistrationAsync(registrationRequest.Name, registrationRequest.Password, var user = await _accountService.RegistrationAsync(registrationRequest.Name, registrationRequest.Password,
invite); invite);
_logger.LogInformation($"Register request for {registrationRequest.Name}");
return Ok(new { token }); _logger.LogInformation($"Register request for {user.Username} with id {user.Id} processed successfully");
var tokens = await _userSession.OpenSessionAsync(user, registrationRequest.DeviceInfo);
_logger.LogInformation($"Session for user {user.Username} with id {user.Id} has been opened");
return Ok(tokens);
} }
catch (UserAlreadyExistException ex) catch (UserAlreadyExistException ex)
{ {
@@ -62,34 +74,37 @@ public class AuthController : Controller
} }
} }
//[RequireHttps]
[HttpPost("login")]// api/auth/login [HttpPost("login")]// api/auth/login
[RequireHttps] public async Task<IActionResult> Login([FromBody] LoginRequest loginRequest)
public async Task<IActionResult> Login([FromBody] LoginRequest userRequest)
{ {
try try
{ {
if (!ModelState.IsValid) if (!ModelState.IsValid)
{
return BadRequest(ModelState); return BadRequest(ModelState);
}
var token = await _accountService.LoginAsync(userRequest.Name, userRequest.Password); var user = await _accountService.LoginAsync(loginRequest.Name, loginRequest.Password);
_logger.LogInformation($"Login request for {userRequest.Name}"); _logger.LogInformation($"Login request for {user.Username} with id {user.Id} processed successfully");
return Ok(new { token });
var tokens = await _userSession.OpenSessionAsync(user, loginRequest.DeviceInfo);
_logger.LogInformation($"Session for user {user.Username} with id {user.Id} has been opened");
return Ok(tokens);
} }
catch (UserNotRegisteredException ex) catch (UserNotRegisteredException ex)
{ {
_logger.LogWarning(ex, "Login failed for user {Name}", userRequest.Name); _logger.LogWarning(ex, "Login failed for user {Name}", loginRequest.Name);
return BadRequest("Login failed: user does not exist."); return BadRequest("Login failed: user does not exist.");
} }
catch (LoginUserException ex) catch (LoginUserException ex)
{ {
_logger.LogWarning(ex, "Login failed for user {Name}", userRequest.Name); _logger.LogWarning(ex, "Login failed for user {Name}", loginRequest.Name);
return BadRequest("Login failed: username or password is incorrect."); return BadRequest("Login failed: username or password is incorrect.");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Unexpected error during login for user {Name}", userRequest.Name); _logger.LogError(ex, "Unexpected error during login for user {Name}", loginRequest.Name);
return StatusCode(500, "An unexpected error occurred. Please try again later."); return StatusCode(500, "An unexpected error occurred. Please try again later.");
} }
} }
@@ -0,0 +1,61 @@
using Govor.Application.Interfaces.UserSession;
using Govor.Contracts.Requests;
using Govor.Contracts.Responses;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Govor.API.Controllers.Authentication;
[ApiController]
[AllowAnonymous]
[Route("api/auth/token")]
public class RefreshController : Controller
{
private readonly ILogger<RefreshController> _logger;
private readonly IUserSessionRefresher _userSession;
public RefreshController(
ILogger<RefreshController> logger,
IUserSessionRefresher userSession)
{
_logger = logger;
_userSession = userSession;
}
//[RequireHttps]
[HttpPost("refresh")] // api/auth/token/refresh
public async Task<IActionResult> Refresh([FromBody] RefreshTokenRequest refreshRequest)
{
try
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
if (string.IsNullOrEmpty(refreshRequest.RefreshToken))
throw new InvalidOperationException("Refresh token cant be empty.");
var result = await _userSession.RefreshTokenAsync(refreshRequest.RefreshToken);
return Ok(new RefreshTokenResponse()
{
AccessToken = result.accessToken,
RefreshToken = result.refreshToken
});
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, "Invalid refresh token.");
return BadRequest(ex.Message);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Refresh token failed.");
return Unauthorized("Invalid refresh token");
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, "An unexpected error occurred.");
}
}
}
@@ -0,0 +1,110 @@
using Govor.Application.Interfaces.Friends;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Application.Interfaces.UserSession;
using Govor.Application.Interfaces.UserSession.Crypto;
using Govor.Contracts.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Govor.API.Controllers.Authentication;
[RequireHttps]
[ApiController]
[Route("api/session")]
[Authorize(Roles = "Admin, User")]
public class SessionKeysController : Controller
{
private readonly ILogger<SessionKeysController> _logger;
private readonly IFriendshipService _friendshipService;
private readonly ICurrentUserSessionService _currentSession;
private readonly ISessionKeyAttacher _sessionKeyAttacher;
private readonly ISessionKeysReader _sessionKeysReader;
private readonly IOneTimePreKeysRotator _oneTimePreKeysRotator;
private readonly ICurrentUserService _currentUser;
public SessionKeysController(
ILogger<SessionKeysController> logger,
IFriendshipService friendshipService,
ICurrentUserSessionService currentSession,
ISessionKeyAttacher sessionKeyAttacher,
ISessionKeysReader sessionKeysReader,
ICurrentUserService currentUser)
{
_logger = logger;
_friendshipService = friendshipService;
_currentSession = currentSession;
_sessionKeyAttacher = sessionKeyAttacher;
_sessionKeysReader = sessionKeysReader;
_currentUser = currentUser;
}
[HttpPost("keys")]
public async Task<IActionResult> UploadSessionKeys([FromBody] UploadKeysRequest request)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
if(request.OneTimePreKeys.Count > 100)
return BadRequest("Too many one time pre keys");
var sessionId = _currentSession.GetUserSessionId();
await _sessionKeyAttacher.AttachKeysAsync(sessionId,
request.IdentityKey,
request.SignedPreKey,
request.SignedPreKeySignature,
request.OneTimePreKeys);
return Ok();
}
[HttpGet("users/{userId}/keys")]
public async Task<IActionResult> GetUserPublicKeys(Guid userId)
{
var requesterId = _currentUser.GetCurrentUserId();
if (!(await _friendshipService.GetFriendsAsync(userId)).Select(u => u.Id).Contains(requesterId))
return Forbid("You can only access keys of your friends");
var keys = await _sessionKeysReader.GetAllActiveKeysAsync(userId);
return Ok(keys);
}
[HttpPost("keys/rotate")]
public async Task<IActionResult> RotateOneTimePreKeys([FromBody] RotateOneTimePreKeysRequest request)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
if (request.NewOneTimePreKeys.Count > 100)
return BadRequest("Too many new one time pre keys");
var sessionId = _currentSession.GetUserSessionId();
await _oneTimePreKeysRotator.RotateOneTimePreKeysAsync(sessionId, request.NewOneTimePreKeys);
return Ok("One-Time PreKeys rotated successfully.");
}
[HttpGet("keys/remaining")]
public async Task<IActionResult> GetRemainingOneTimePreKeysCount()
{
var sessionId = _currentSession.GetUserSessionId();
var count = await _sessionKeysReader.GetRemainingOneTimePreKeysCountAsync(sessionId);
return Ok(new { remaining = count });
}
[HttpPost("keys/{preKeyId}/used")]
public async Task<IActionResult> MarkOneTimePreKeyAsUsed([FromRoute] Guid preKeyId)
{
var sessionId = _currentSession.GetUserSessionId();
await _oneTimePreKeysRotator.MarkOneTimePreKeyAsUsedAsync(sessionId, preKeyId);
return Ok("Marked as used.");
}
}
+125
View File
@@ -0,0 +1,125 @@
using AutoMapper;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Contracts.Requests;
using Govor.Contracts.Responses;
using Govor.Data.Repositories.Exceptions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Govor.API.Controllers;
[ApiController]
[Authorize(Roles = "Admin, User")]
[Route("api/chats")]
public class ChatLoadController : Controller
{
private readonly ICurrentUserService _currentUser;
private readonly ILogger<ChatLoadController> _logger;
private readonly IMessagesLoader _messagesLoader;
private readonly IMapper _mapper;
public ChatLoadController(
ILogger<ChatLoadController> logger,
IMessagesLoader messagesLoader,
ICurrentUserService currentUser,
IMapper mapper)
{
_logger = logger;
_messagesLoader = messagesLoader;
_currentUser = currentUser;
_mapper = mapper;
}
[HttpGet("group-messages")]
public async Task<IActionResult> GetGroupMessages(
Guid chatId,
[FromQuery] MessageQuery query)
{
try
{
if (query.Before < 0 || query.After < 0 || query.After + query.Before > 100)
return BadRequest("Values must be non-negative and total must not exceed 100.");
var result = await _messagesLoader.LoadMessagesInChatGroup(
chatId,
_currentUser.GetCurrentUserId(),
query.StartMessageId,
query.Before,
query.After);
var response = _mapper.Map<List<MessageResponse>>(result);
return Ok(response);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex.Message);
return Forbid(ex.Message);
}
catch (NotFoundException ex)
{
_logger.LogWarning(ex, ex.Message);
return BadRequest(ex.Message);
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, ex.Message);
return BadRequest(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, "Unexpected Error! Please try again later.");
}
}
[HttpGet("user-messages")]
public async Task<IActionResult> GetUserMessages(
Guid userId,
[FromQuery] MessageQuery query)
{
try
{
if (query.Before < 0 || query.After < 0 || query.After + query.Before > 100)
return BadRequest("Values must be non-negative and total must not exceed 100.");
var result = await _messagesLoader.LoadMessagesInUserChat(
userId,
_currentUser.GetCurrentUserId(),
query.StartMessageId,
query.Before,
query.After);
var response = _mapper.Map<List<MessageResponse>>(result);
return Ok(response);
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, ex.Message);
return BadRequest(ex.Message);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex.Message);
return Forbid(ex.Message);
}
catch (NotFoundException ex)
{
_logger.LogWarning(ex, ex.Message);
return BadRequest(ex.Message);
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, ex.Message);
return BadRequest(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, "Unexpected Error! Please try again later.");
}
}
}
@@ -0,0 +1,91 @@
using AutoMapper;
using Govor.Application.Interfaces.Friends;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Contracts.DTOs;
using Govor.Core.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Govor.API.Controllers.Friends;
[Authorize]
[Route("api/friends")]
[ApiController]
public class FriendsRequestQueryController : Controller
{
private readonly ILogger<FriendsRequestQueryController> _logger;
private readonly IFriendRequestQueryService _friendsService;
private readonly ICurrentUserService _currentUserService;
private readonly IMapper _mapper;
public FriendsRequestQueryController(
ILogger<FriendsRequestQueryController> logger,
IFriendRequestQueryService friendsService,
ICurrentUserService currentUserService,
IMapper mapper)
{
_logger = logger;
_mapper = mapper;
_friendsService = friendsService;
_currentUserService = currentUserService;
}
[HttpGet("requests")] // api/friends/requests
public async Task<IActionResult> GetIncomingRequests()
{
try
{
var result = await _friendsService.GetIncomingAsync(_currentUserService.GetCurrentUserId());
var response = _mapper.Map<List<FriendshipDto>>(result);
return Ok(response);
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, ex.Message);
return Ok(new List<FriendshipDto>());
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, ex.Message);
return Forbid(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, new { error = "Internal server error." });
}
}
[HttpGet("responses")]// api/friends/responses
public async Task<IActionResult> GetResponses()
{
try
{
var userId = _currentUserService.GetCurrentUserId();
_logger.LogInformation("Getting responses by user {userId}", userId);
var result = await _friendsService.GetResponsesAsync(userId);
var response = _mapper.Map<List<FriendshipDto>>(result);
return Ok(response);
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, ex.Message);
return Ok(new List<FriendshipDto>());
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, ex.Message);
return Forbid(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, new { error = "Internal server error." });
}
}
}
@@ -0,0 +1,84 @@
using AutoMapper;
using Govor.Application.Exceptions.FriendsService;
using Govor.Application.Interfaces.Friends;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Contracts.DTOs;
using Govor.Core.Models.Users;
using Microsoft.AspNetCore.Mvc;
namespace Govor.API.Controllers.Friends;
[Route("api/friends")]
public class FriendshipController : Controller
{
private readonly ILogger<FriendshipController> _logger;
private readonly IFriendshipService _friendsService;
private readonly ICurrentUserService _currentUserService;
private readonly IMapper _mapper;
public FriendshipController(
ILogger<FriendshipController> logger,
IFriendshipService friendsService,
ICurrentUserService currentUserService,
IMapper mapper)
{
_logger = logger;
_mapper = mapper;
_friendsService = friendsService;
_currentUserService = currentUserService;
}
[HttpGet("search")] // api/friends/search?query=
public async Task<IActionResult> Search(string query)
{
if (string.IsNullOrWhiteSpace(query))
return BadRequest("Query cannot be empty");
try
{
var result = await _friendsService.SearchUsersAsync(query, _currentUserService.GetCurrentUserId());
var response = _mapper.Map<List<UserDto>>(result);
return Ok(response);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, ex.Message);
return Forbid(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, new { error = "Internal error during user search." });
}
}
[HttpGet] // api/friends
public async Task<IActionResult> GetFriends()
{
try
{
var result = await _friendsService.GetFriendsAsync(_currentUserService.GetCurrentUserId());
var response = _mapper.Map<List<UserDto>>(result);
return Ok(response);
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, ex.Message);
return Ok(Array.Empty<UserDto>());
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, ex.Message);
return Forbid(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, new { error = "Internal server error." });
}
}
}
-164
View File
@@ -1,164 +0,0 @@
using Govor.Application.Exceptions.FriendsService;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Contracts.DTOs;
using Govor.Core.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Govor.API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize(Roles = "User,Admin")]
public class FriendsController : Controller
{
private readonly ILogger<FriendsController> _logger;
private readonly IFriendsService _friendsService;
private readonly ICurrentUserService _currentUserService;
public FriendsController(ILogger<FriendsController> logger,
IFriendsService friendsService,
ICurrentUserService currentUserService)
{
_logger = logger;
_friendsService = friendsService;
_currentUserService = currentUserService;
}
[HttpGet("search")]
public async Task<IActionResult> Search(string query)
{
if (string.IsNullOrWhiteSpace(query))
return BadRequest("Query cannot be empty");
try
{
var result = await _friendsService.SearchUsersAsync(query, _currentUserService.GetCurrentUserId());
return Ok(BuildUserDtos(result));
}
catch (SearchUsersException ex)
{
_logger.LogWarning(ex, ex.Message);
return NotFound(new { error = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, new { error = "Internal error during user search." });
}
}
[HttpPost("request")]
public async Task<IActionResult> SendRequest(Guid targetUserId)
{
if (targetUserId == Guid.Empty)
return BadRequest("Target user ID is invalid");
try
{
await _friendsService.SendFriendRequestAsync(targetUserId, _currentUserService.GetCurrentUserId());
return Ok(new { message = "Friend request sent successfully." });
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, "Tried to send request to self");
return UnprocessableEntity(new { error = ex.Message });
}
catch (RequestAlreadySentException ex)
{
_logger.LogWarning(ex, ex.Message);
return Conflict(new { error = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, new { error = "Failed to send friend request." });
}
}
[HttpGet("requests")]
public async Task<IActionResult> GetIncomingRequests()
{
try
{
var result = await _friendsService.GetIncomingRequestsAsync(_currentUserService.GetCurrentUserId());
return Ok(BuildFriendshipDtos(result));
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, ex.Message);
return BadRequest(new { error = "Failed to get friend requests. User data missing." });
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, new { error = "Internal server error." });
}
}
[HttpPost("accept")]
public async Task<IActionResult> AcceptFriend(Guid requesterId)
{
if (requesterId == Guid.Empty)
return BadRequest("Requester ID is invalid");
try
{
await _friendsService.AcceptFriendRequestAsync(requesterId, _currentUserService.GetCurrentUserId());
return Ok(new { message = "Friend request accepted." });
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, ex.Message);
return NotFound(new { error = ex.Message });
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, ex.Message);
return Forbid();
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, new { error = "Failed to accept friend request." });
}
}
[HttpGet]
public async Task<IActionResult> GetFriends()
{
try
{
var result = await _friendsService.GetFriendsAsync(_currentUserService.GetCurrentUserId());
return Ok(BuildUserDtos(result));
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, ex.Message);
return BadRequest(new { error = "User data not found." });
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, new { error = "Internal server error." });
}
}
private List<UserDto> BuildUserDtos(IEnumerable<User> users) => users.Select(user => new UserDto
{
Id = user.Id,
Username = user.Username,
Description = user.Description,
WasOnline = user.WasOnline,
IconId = user.IconId
}).ToList();
private List<FriendshipDto> BuildFriendshipDtos(IEnumerable<Friendship> friendships) => friendships.Select(f => new FriendshipDto
{
Id = f.Id,
Status = f.Status,
AddresseeId = f.AddresseeId,
RequesterId = f.RequesterId
}).ToList();
}
+19 -6
View File
@@ -1,4 +1,5 @@
using Govor.API.Services; using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -9,18 +10,24 @@ namespace Govor.API.Controllers;
public class InviteController : ControllerBase public class InviteController : ControllerBase
{ {
private readonly IGroupService _groupService; private readonly IGroupService _groupService;
private readonly ILogger<InviteController> _logger;
public InviteController(IGroupService groupService) private readonly ICurrentUserService _currentUser;
public InviteController(IGroupService groupService,
ILogger<InviteController> logger)
{ {
_groupService = groupService; _groupService = groupService;
_logger = logger;
} }
[HttpGet("{code}")]
[Authorize] [Authorize]
[HttpGet("{code}")]
public IActionResult JoinGroup(string code) public IActionResult JoinGroup(string code)
{ {
var group = _groupService.GetGroupByInvite(code); try
if (group == null) return NotFound(); {
_groupService.AddUserToGroupByInvitationAsync(_currentUser.GetCurrentUserId(), code);
var group = _groupService.GetGroupByInviteCode(code);
return Ok(new return Ok(new
{ {
@@ -29,4 +36,10 @@ public class InviteController : ControllerBase
isChannel = group.IsChannel isChannel = group.IsChannel
}); });
} }
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, ex.Message);
}
}
} }
+126
View File
@@ -0,0 +1,126 @@
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Application.Interfaces.Medias;
using Govor.Contracts.Requests;
using Govor.Core.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Govor.API.Controllers;
[ApiController]
[Route("api/media")]
[Authorize(Roles = "User,Admin")]
public class MediaController : Controller
{
private readonly ILogger<MediaController> _logger;
private readonly IMediaService _mediaService;
private readonly IAccesserToDownloadMedia _accesser;
private readonly ICurrentUserService _currentUserService;
public MediaController(
ILogger<MediaController> logger,
IMediaService mediaService,
IAccesserToDownloadMedia accesser,
ICurrentUserService currentUserService)
{
_logger = logger;
_accesser = accesser;
_mediaService = mediaService;
_currentUserService = currentUserService;
}
[HttpPost("upload")]
[RequestSizeLimit(20_000_000)] // ~20MB
public async Task<IActionResult> Upload([FromForm] MediaUploadRequest request)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
if (request?.FromFile is null || request.FromFile.Length == 0)
return BadRequest("No file uploaded");
if (request.FromFile.Length > 20_000_000)
return BadRequest("File is too large");
if (string.IsNullOrWhiteSpace(request.MimeType))
return BadRequest("Missing MIME type");
try
{
byte[] fileBytes = await ReadFileAsync(request.FromFile);
var media = new Media(
_currentUserService.GetCurrentUserId(),
DateTime.UtcNow,
Path.GetFileName(request.FromFile.FileName),
fileBytes,
request.Type,
request.MimeType,
request.EncryptedKey,
request.OwnerType,
null)
{
OwnerType = request.OwnerType,
OwnerId = request.OwnerType == MediaOwnerType.Avatar
? _currentUserService.GetCurrentUserId()
: null,
};
var result = await _mediaService.UploadMediaAsync(media);
_logger.LogInformation("Uploaded file {FileName} from user {UserId}",
media.FileName, media.UploaderId);
return Ok(result);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, ex.Message);
return Forbid(ex.Message);
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, ex.Message);
return BadRequest(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error uploading media");
return StatusCode(500, "Internal server error");
}
}
private async Task<byte[]> ReadFileAsync(IFormFile file)
{
await using var ms = new MemoryStream();
await file.CopyToAsync(ms);
return ms.ToArray();
}
[HttpGet("download/{id}")]
public async Task<IActionResult> Download(Guid id)
{
try
{
var userId = _currentUserService.GetCurrentUserId();
if (!await _accesser.HasAccessAsync(id, userId))
return Forbid();
var media = await _mediaService.GetMediaByIdAsync(id);
return File(media.Data, media.MimeType, Path.GetFileName(media.FileName));
}
catch (KeyNotFoundException ex)
{
_logger.LogWarning(ex, ex.Message);
return NotFound("Media not found");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error downloading media");
return StatusCode(500, "Internal server error");
}
}
}
@@ -0,0 +1,79 @@
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Application.Interfaces.UserOnlineStatus;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Govor.API.Controllers;
[ApiController]
[Route("api/online")]
[Authorize(Roles = "User,Admin")]
public class OnlinePingingController : Controller
{
private readonly ILogger<OnlinePingingController> _logger;
private readonly IPingHandlerService _ping;
private readonly IUserPresenceReader _presenceReader;
private readonly IOnlineUserStore _userOnlineStore;
private readonly ICurrentUserService _currentUserService;
public OnlinePingingController(ILogger<OnlinePingingController> logger,
IPingHandlerService ping,
ICurrentUserService currentUserService)
{
_logger = logger;
_ping = ping;
_currentUserService = currentUserService;
}
[HttpPatch("ping")]// api/online/ping
public async Task<IActionResult> Ping()
{
try
{
_logger.LogInformation("Ping...");
var id = _currentUserService.GetCurrentUserId();
await _ping.Ping(id);
_logger.LogInformation($"Ping from user {id} processed successfully");
return Ok();
}
catch (InvalidOperationException e)
{
_logger.LogError(e, e.Message);
return BadRequest("User can't be found in our database.");
}
catch (UnauthorizedAccessException e)
{
_logger.LogError(e, e.Message);
return Forbid(e.Message);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
return StatusCode(500, "Failed to ping.");
}
}
[HttpGet("status/{userId}")]
public async Task<IActionResult> GetStatus(Guid userId)
{
try
{
var isOnline = _userOnlineStore.IsOnline(userId);
var lastSeen = await _presenceReader.GetLastSeenAsync(userId);
return Ok(new {
isOnline,
lastSeen
});
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
return StatusCode(500, "Internal server error.");
}
}
}
+152
View File
@@ -0,0 +1,152 @@
using AutoMapper;
using Govor.API.Hubs;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Application.Interfaces.Medias;
using Govor.Contracts.DTOs;
using Govor.Contracts.Requests;
using Govor.Core.Models;
using Govor.Data.Repositories.Exceptions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
namespace Govor.API.Controllers;
[ApiController]
[Route("api/profile")]
[Authorize(Roles = "Admin,User")]
public class ProfileController : ControllerBase
{
private readonly ILogger<ProfileController> _logger;
private readonly IMediaService _mediaService;
private readonly IProfileService _profileService;
private readonly ICurrentUserService _currentUserService;
private readonly IHubContext<ProfileHub> _profileHubContext;
private readonly IMapper _mapper;
public ProfileController(
ILogger<ProfileController> logger,
IMediaService mediaService,
IProfileService profileService,
ICurrentUserService currentUserService,
IHubContext<ProfileHub> profileHubContext,
IMapper mapper)
{
_logger = logger;
_mediaService = mediaService;
_profileService = profileService;
_profileHubContext = profileHubContext;
_currentUserService = currentUserService;
_mapper = mapper;
}
[HttpPost("avatar")] // api/profile/avatar
public async Task<IActionResult> UploadAvatar([FromForm] AvatarUploadRequest request)
{
var userId = _currentUserService.GetCurrentUserId();
if (request.FromFile == null || request.FromFile.Length == 0)
{
return BadRequest("File is empty.");
}
try
{
byte[] fileBytes = await ReadFileAsync(request.FromFile);
var media = new Media(
_currentUserService.GetCurrentUserId(),
DateTime.UtcNow,
Path.GetFileName(request.FromFile.FileName),
fileBytes,
request.Type,
request.MimeType,
String.Empty,
MediaOwnerType.Avatar,
userId);
var mediaInfo = await _mediaService.UploadMediaAsync(media);
await _profileService.SetNewIcon(userId, mediaInfo.MediaId);
var iconId = mediaInfo.MediaId;
var payload = new { userId, iconId };
await _profileHubContext.Clients.All.SendAsync(
"AvatarUpdated",
payload
);
return Ok(mediaInfo);
}
catch (System.Exception ex)
{
return StatusCode(500, new { Message = "Avatar processing error.", Details = ex.Message });
}
}
private async Task<byte[]> ReadFileAsync(IFormFile file)
{
await using var ms = new MemoryStream();
await file.CopyToAsync(ms);
return ms.ToArray();
}
[HttpGet("download/me")]
public async Task<IActionResult> DownloadProfile()
{
try
{
var userId = _currentUserService.GetCurrentUserId();
var user = await _profileService.GetUserProfileAsync(userId);
var dto = _mapper.Map<UserProfileDto>(user);
return Ok(dto);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex.Message);
return Forbid(ex.Message);
}
catch (NotFoundException ex)
{
_logger.LogWarning(ex, ex.Message);
return NotFound("Profile not found");
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, "Unexpected Error! Please try again later.");
}
}
[HttpGet("download/{id}")]
public async Task<IActionResult> DowloadProfileByUserId(Guid id)
{
try
{
var user = await _profileService.GetUserProfileAsync(id);
var dto = _mapper.Map<UserProfileDto>(user);
return Ok(dto);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex.Message);
return Forbid(ex.Message);
}
catch (NotFoundException ex)
{
_logger.LogWarning(ex, ex.Message);
return NotFound("Profile not found");
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, "Unexpected Error! Please try again later.");
}
}
}
+145
View File
@@ -0,0 +1,145 @@
using AutoMapper;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Application.Interfaces.UserSession;
using Govor.Contracts.DTOs;
using Govor.Data.Repositories.Exceptions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Govor.API.Controllers;
[ApiController]
[Authorize(Roles = "Admin,User")]
[Route("api/[controller]")]
public class SessionController : Controller
{
private readonly ILogger<SessionController> _logger;
private readonly ICurrentUserService _currentUserService;
private readonly ICurrentUserSessionService _currentUserSessionService;
private readonly IUserSessionReader _userSessionReader;
private readonly IUserSessionRevoker _userSessionRevoker;
private readonly IMapper _mapper;
public SessionController(
ILogger<SessionController> logger,
ICurrentUserService currentUserService,
ICurrentUserSessionService currentUserSessionService,
IUserSessionReader userSessionReader,
IUserSessionRevoker userSessionRevoker,
IMapper mapper)
{
_logger = logger;
_currentUserService = currentUserService;
_currentUserSessionService = currentUserSessionService;
_userSessionReader = userSessionReader;
_userSessionRevoker = userSessionRevoker;
_mapper = mapper;
}
[HttpGet("all")]
public async Task<IActionResult> GetAllSessions()
{
try
{
var sessions = await _userSessionReader.GetAllSessionsAsync(_currentUserService.GetCurrentUserId());
return Ok(_mapper.Map<List<SessionDto>>(sessions));
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, ex.Message);
return Forbid(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, "Unexpected Error! Please try again later.");
}
}
[HttpDelete("close/{sessionId}")]
public async Task<IActionResult> CloseSession(Guid sessionId)
{
try
{
if (sessionId == Guid.Empty)
return BadRequest("Invalid sessionId.");
await _userSessionRevoker.CloseSessionByIdAsync(sessionId,
_currentUserService.GetCurrentUserId());
return Ok();
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, ex.Message);
return BadRequest(ex.Message);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, ex.Message);
return Forbid(ex.Message);
}
catch (NotFoundException ex)
{
_logger.LogWarning(ex, ex.Message);
return NotFound(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, "Unexpected Error! Please try again later.");
}
}
[HttpDelete("close")]
public async Task<IActionResult> CloseCurrent()
{
try
{
await _userSessionRevoker.CloseSessionByIdAsync(
_currentUserSessionService.GetUserSessionId(),
_currentUserService.GetCurrentUserId());
return Ok();
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, ex.Message);
return BadRequest(ex.Message);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, ex.Message);
return Forbid(ex.Message);
}
catch (NotFoundException ex)
{
_logger.LogWarning(ex, ex.Message);
return NotFound(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, "Unexpected Error! Please try again later.");
}
}
[HttpDelete("close/all")]
public async Task<IActionResult> CloseAllSessions()
{
try
{
await _userSessionRevoker.CloseAllSessionsAsync(_currentUserService.GetCurrentUserId());
return Ok();
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, ex.Message);
return Forbid(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return StatusCode(500, "Unexpected Error! Please try again later.");
}
}
}
@@ -1,77 +0,0 @@
using Govor.API.Services.AdminsStuff.Interfaces;
using Govor.API.Services.Authentication.Interfaces;
using Govor.Application.Infrastructure.Extensions;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.AdminsStuff;
using Govor.Application.Interfaces.Authentication;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Application.Services;
using Govor.Application.Validators;
using Govor.Core.Infrastructure.Extensions;
using Govor.Core.Infrastructure.Validators;
using Govor.Core.Models;
using Govor.Core.Repositories.Admins;
using Govor.Core.Repositories.Friendships;
using Govor.Core.Repositories.Invaites;
using Govor.Core.Repositories.MediasAttachments;
using Govor.Core.Repositories.Messages;
using Govor.Core.Repositories.Users;
using Govor.Data;
using Govor.Data.Repositories;
using Microsoft.EntityFrameworkCore;
namespace Govor.API.Extensions;
public static class ConfigurationProgramExtensions
{
public static void AddServices(this IServiceCollection services)
{
services.AddScoped<IPasswordHasher, PasswordHasher>();
services.AddScoped<IJwtService, JwtService>();
services.AddScoped<IAccountService, AuthService>();
services.AddScoped<IUsersAdministration, UsersService>();
services.AddScoped<IInvitesService, InvitesService>();
services.AddScoped<IInvitationGenerator, InvitationGenerator>();
services.AddScoped<IUsernameValidator, UsernameValidator>();
services.AddScoped<IFriendsService, FriendsService>();
services.AddScoped<IStorageService>(sp =>
{
var env = sp.GetRequiredService<IWebHostEnvironment>();
return new LocalStorageService(env.ContentRootPath);
});
services.AddHttpContextAccessor(); // it's very important for CurrentUserService
services.AddScoped<ICurrentUserService, CurrentUserService>();
}
public static void AddRepositories(this IServiceCollection services)
{
services.AddScoped<IUsersRepository, UsersRepository>();
services.AddScoped<IMessagesRepository, MessagesRepository>();
services.AddScoped<IInvitesRepository, InvitesRepository>();
services.AddScoped<IAdminsRepository, AdminsRepository>();
services.AddScoped<IMediaAttachmentsRepository, MediaAttachmentsRepository>();
services.AddScoped<IFriendshipsRepository, FriendshipsRepository>();
}
public static void AddValidators(this IServiceCollection services)
{
services.AddScoped<IObjectValidator<User>, UserValidator>();
services.AddScoped<IObjectValidator<Message>, MessageValidator>();
services.AddScoped<IObjectValidator<MediaAttachments>, MediaAttachmentsValidator>();
services.AddScoped<IObjectValidator<Admin>, AdminValidator>();
services.AddScoped<IObjectValidator<Invitation>, InvitationValidator>();
services.AddScoped<IObjectValidator<Friendship>, FriendshipValidator>();
}
public static void AddGovorDbContext(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<GovorDbContext>(
options =>
{
options.UseNpgsql(configuration.GetConnectionString(nameof(GovorDbContext)));
}
);
}
}
+76
View File
@@ -0,0 +1,76 @@
using Govor.Contracts.Responses.SignalR;
using Microsoft.AspNetCore.SignalR;
namespace Govor.API.Filters;
public class HubExceptionFilter : IHubFilter
{
private readonly ILogger<HubExceptionFilter> _logger;
public HubExceptionFilter(ILogger<HubExceptionFilter> logger)
{
_logger = logger;
}
public async ValueTask<object?> InvokeMethodAsync(
HubInvocationContext context,
Func<HubInvocationContext, ValueTask<object?>> next)
{
try
{
return await next(context);
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, "Bad request in {Method}", context.HubMethodName);
return CreateHubErrorResult(context, HubResultStatus.BadRequest, ex.Message);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Unauthorized in {Method}", context.HubMethodName);
return CreateHubErrorResult(context, HubResultStatus.Unauthorized, ex.Message);
}
catch (KeyNotFoundException ex)
{
_logger.LogWarning(ex, "Not found in {Method}", context.HubMethodName);
return CreateHubErrorResult(context, HubResultStatus.NotFound, ex.Message);
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, "Invalid operation in {Method}", context.HubMethodName);
return CreateHubErrorResult(context, HubResultStatus.Conflict, ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled error in {Method}", context.HubMethodName);
return CreateHubErrorResult(context, HubResultStatus.ServerError, "Internal server error");
}
}
private static object CreateHubErrorResult(HubInvocationContext context, HubResultStatus status, string message)
{
var returnType = context.HubMethod.ReturnType;
// Поддержка Task<HubResult<T>>
if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>))
{
var resultType = returnType.GenericTypeArguments[0];
if (resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(HubResult<>))
{
var hubResultType = resultType;
var errorResult = Activator.CreateInstance(hubResultType)!;
hubResultType.GetProperty(nameof(HubResult<object>.Status))!
.SetValue(errorResult, status);
hubResultType.GetProperty(nameof(HubResult<object>.ErrorMessage))!
.SetValue(errorResult, message);
return Task.FromResult(errorResult);
}
}
// Ничего не возвращаем, если не поддерживается
return null;
}
}
+16 -12
View File
@@ -1,24 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="14.0.0" /> <PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6" />
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="NSwag.AspNetCore" Version="14.4.0" /> <PackageReference Include="NSwag.AspNetCore" Version="14.4.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -26,4 +25,9 @@
<ProjectReference Include="..\Govor.Contracts\Govor.Contracts.csproj" /> <ProjectReference Include="..\Govor.Contracts\Govor.Contracts.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Contracts\" />
</ItemGroup>
</Project> </Project>
+265 -52
View File
@@ -1,103 +1,316 @@
using System.Security.Claims; using Govor.API.Common.SignalR.Helpers;
using Govor.Core.Models; using Govor.Application.Exceptions.VerifyFriendship;
using Govor.Core.Repositories.Users; using Govor.Application.Interfaces;
using Govor.Data.Repositories.Exceptions; using Govor.Application.Interfaces.Messages;
using Govor.Application.Interfaces.Messages.Parameters;
using Govor.Contracts.Requests.SignalR;
using Govor.Contracts.Responses.SignalR;
using Govor.Core.Models.Messages;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
namespace Govor.API.Hubs; namespace Govor.API.Hubs;
[Authorize] [Authorize] // api/chats
public class ChatsHub : Hub public class ChatsHub : Hub
{ {
private readonly IUsersRepository _usersRepository;
private readonly ILogger<ChatsHub> _logger; private readonly ILogger<ChatsHub> _logger;
private readonly IMessageCommandService _messageCommandService;
private readonly IUserGroupsService _userService;
private readonly IHubUserAccessor _userAccessor;
public ChatsHub(IUsersRepository usersRepository, ILogger<ChatsHub> logger) public ChatsHub(
ILogger<ChatsHub> logger,
IMessageCommandService messageCommandService,
IUserGroupsService userService,
IHubUserAccessor userAccessor)
{ {
_usersRepository = usersRepository;
_logger = logger; _logger = logger;
_messageCommandService = messageCommandService;
_userService = userService;
_userAccessor = userAccessor;
} }
public override async Task OnConnectedAsync() public override async Task OnConnectedAsync()
{ {
var userId = GetUserId(); var userId = _userAccessor.GetUserId(Context);
if (userId != Guid.Empty) if (userId == Guid.Empty)
{ {
// Привязываем ConnectionId к UserId _logger.LogWarning("User connected with invalid UserID claim.");
await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString()); Context.Abort();
_logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId}", userId, Context.ConnectionId); return;
} }
await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString());
_logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId} and added to their group",
userId, Context.ConnectionId);
var userGroups = await _userService.GetUserGroupsAsync(userId);
foreach (var group in userGroups)
{
await Groups.AddToGroupAsync(Context.ConnectionId, $"group_{group.Id}");
}
await base.OnConnectedAsync(); await base.OnConnectedAsync();
} }
public override async Task OnDisconnectedAsync(Exception? exception) public override async Task OnDisconnectedAsync(Exception exception)
{ {
var userId = GetUserId(); var userId = _userAccessor.GetUserId(Context, true);
if (userId != Guid.Empty) if (userId != Guid.Empty)
{ {
await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString()); await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString());
_logger.LogInformation("User {UserId} disconnected", userId); _logger.LogInformation("User {UserId} disconnected with ConnectionId {ConnectionId} and removed from their group",
userId, Context.ConnectionId);
var userGroups = await _userService.GetUserGroupsAsync(userId);
foreach (var group in userGroups)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"group_{group.Id}");
} }
}
else if (exception != null)
{
_logger.LogWarning(exception,
"User disconnected with an exception and invalid UserID claim. ConnectionId: {ConnectionId}",
Context.ConnectionId);
}
else
{
_logger.LogInformation(
"User disconnected with no exception and invalid UserID claim. ConnectionId: {ConnectionId}",
Context.ConnectionId);
}
await base.OnDisconnectedAsync(exception); await base.OnDisconnectedAsync(exception);
} }
public async Task Edit(string newMessage, Guid messageId) public async Task<HubResult<UserMessageResponse>> Send(MessageRequest request)
{ {
var senderId = _userAccessor.GetUserId(Context);
if (string.IsNullOrWhiteSpace(request.EncryptedContent) &&
(request.MediaAttachments == null || !request.MediaAttachments.Any()))
{
_logger.LogWarning("Empty message received from user {UserId}", senderId);
return HubResult<UserMessageResponse>.BadRequest("Message must contain content or media.");
} }
public async Task Send(string message, Guid toUserId) if (request.EncryptedContent.Length > 50_000)
{ {
// Валидация входных данных _logger.LogWarning("User {SenderId} tried to send a too long message (length: {Length})", senderId, request.EncryptedContent.Length);
if (string.IsNullOrWhiteSpace(message)) return HubResult<UserMessageResponse>.BadRequest("Message cannot exceed 50,000 characters.");
{
_logger.LogWarning("Empty message received from user {UserId}", GetUserId());
throw new ArgumentException("Message cannot be empty", nameof(message));
} }
var senderId = GetUserId(); _logger.LogInformation("Sending message from {SenderId} to {RecipientId} ({RecipientType})",
senderId, request.RecipientId, request.RecipientType);
// Проверка существования получателя var sendMessageParams = new SendMessage(
try EncryptContent: request.EncryptedContent,
{ ReplyToMessageId: request.ReplyToMessageId,
await _usersRepository.FindByIdAsync(toUserId); FromUserId: senderId,
} RecipientId: request.RecipientId,
catch (NotFoundByKeyException<User> ex) RecipientType: request.RecipientType,
{ SendAt: DateTime.UtcNow,
_logger.LogWarning("Recipient user {ToUserId} not found", toUserId); Media: request.MediaAttachments?.Select(f => new SendMedia(f.MediaId, f.EncryptedKey)) ??
throw; Array.Empty<SendMedia>()
} );
catch (ArgumentException ex)
{
_logger.LogWarning("Invalid recipient userId received from user {UserId}", GetUserId());
throw;
}
try try
{ {
_logger.LogInformation("Message sent from {SenderId} to {RecipientId}: {Message}", senderId, toUserId, message); var result = await _messageCommandService.SendMessageAsync(sendMessageParams);
// Отправка сообщения отправителю и получателю if (!result.IsSuccess || result.Message.Id == Guid.Empty)
await Clients.Group(toUserId.ToString()).SendAsync("Receive", message, senderId); return LogAndError<UserMessageResponse>(senderId, request.RecipientId, "Failed to send message", result.Exception);
await Clients.Group(senderId.ToString()).SendAsync("Receive", message, senderId);
var response = BuildUserMessageResponse(result.Message, request.ReplyToMessageId);
await NotifyClientsAboutMessage(response);
return HubResult<UserMessageResponse>.Ok(response);
}
catch (UnauthorizedAccessException ex)
{
return LogAndUnauthorized<UserMessageResponse>(ex, "Unauthorized sending attempt", senderId,
request.RecipientId);
}
catch (FriendshipException)
{
return HubResult<UserMessageResponse>.Unauthorized(
"You cannot send this message because you are not friends.");
}
catch (ArgumentException)
{
return HubResult<UserMessageResponse>.BadRequest("Invalid message content.");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error sending message from {SenderId} to {RecipientId}", senderId, toUserId); return LogAndError<UserMessageResponse>(senderId, request.RecipientId, "Unhandled exception", ex);
throw;
} }
} }
private Guid GetUserId() public async Task<HubResult<MessageRemovedResponse>> Remove(RemoveMessageRequest request)
{ {
var userIdClaim = Context.User?.FindFirst("userID")?.Value; var removerId = _userAccessor.GetUserId(Context);
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId)) _logger.LogInformation("Removing message {MessageId} by user {RemoverId}", request.MessageId, removerId);
try
{ {
_logger.LogError("Could not retrieve sender userId"); var result = await _messageCommandService.DeleteMessageAsync(new DeleteMessage(removerId, request.MessageId));
throw new UnauthorizedAccessException("userID claim is missing or invalid");
if (!result.IsSuccess || result.OriginalMessage == null)
return LogAndError<MessageRemovedResponse>(removerId, request.MessageId, "Message deletion failed", result.Exception);
var notification = new MessageRemovedResponse
{
MessageId = request.MessageId,
SenderId = result.OriginalMessage.SenderId,
RecipientId = result.OriginalMessage.RecipientId,
RecipientType = result.OriginalMessage.RecipientType
};
await NotifyClientsAboutRemoval(notification);
_logger.LogInformation("Message {MessageId} removed successfully by {RemoverId}", request.MessageId, removerId);
return HubResult<MessageRemovedResponse>.Ok(notification);
} }
return userId; catch (UnauthorizedAccessException ex)
{
return LogAndUnauthorized<MessageRemovedResponse>(ex, "Unauthorized removal", removerId, request.MessageId);
} }
catch (KeyNotFoundException ex)
{
return LogAndNotFound<MessageRemovedResponse>(ex, "Message not found", removerId, request.MessageId);
}
catch (Exception ex)
{
return LogAndError<MessageRemovedResponse>(removerId, request.MessageId, "Unhandled deletion error", ex);
}
}
public async Task<HubResult<MessageEditResponse>> Edit(EditMessageRequest request)
{
var editor = _userAccessor.GetUserId(Context);
_logger.LogInformation("Editing message {MessageId} by user {EditorId}", request.MessageId, editor);
var editMessageParam = new EditMessage(editor,
request.MessageId,
request.NewEncryptedContent,
DateTime.UtcNow);
try
{
var result = await _messageCommandService.EditMessageAsync(editMessageParam);
if (!result.IsSuccess || result.OriginalMessage == null)
return LogAndError<MessageEditResponse>(editor, request.MessageId, "Edit message error",
result.Exception);
var response = new MessageEditResponse()
{
MessageId = result.messageId,
EditorId = editor,
RecipientId = result.OriginalMessage.RecipientId,
RecipientType = result.OriginalMessage.RecipientType,
NewEncryptedContent = request.NewEncryptedContent,
EditedAt = editMessageParam.EditedAt,
};
await NotifyClientsAboutEdit(response);
_logger.LogInformation("Message {MessageId} edited successfully by {editor}", request.MessageId, editor);
return HubResult<MessageEditResponse>.Ok(response);
}
catch (UnauthorizedAccessException ex)
{
return LogAndUnauthorized<MessageEditResponse>(ex, "Unauthorized editing", editor, request.MessageId);
}
catch (KeyNotFoundException ex)
{
return LogAndNotFound<MessageEditResponse>(ex, "Message not found", editor, request.MessageId);
}
catch (Exception ex)
{
return LogAndError<MessageEditResponse>(editor, request.MessageId, "Unhandled exception error", ex);
}
}
#region common
private UserMessageResponse BuildUserMessageResponse(Message message, Guid? replyToId)
{
return new UserMessageResponse
{
MessageId = message.Id,
SenderId = message.SenderId,
RecipientId = message.RecipientId,
RecipientType = message.RecipientType,
EncryptedContent = message.EncryptedContent,
SentAt = message.SentAt,
IsEdited = false,
MediaAttachments = message.MediaAttachments.Select(m => m.MediaFile).ToList(),
ReplyToMessageId = replyToId
};
}
private async Task NotifyClientsAboutMessage(UserMessageResponse response)
{
string group = response.RecipientType == RecipientType.User
? response.RecipientId.ToString()
: $"group_{response.RecipientId}";
await Clients.Group(group).SendAsync("ReceiveMessage", response);
await Clients.Caller.SendAsync("MessageSent", response);
}
private async Task NotifyClientsAboutRemoval(MessageRemovedResponse response)
{
if (response.RecipientType == RecipientType.User)
{
await Clients.Group(response.SenderId.ToString()).SendAsync("MessageRemoved", response);
if (response.SenderId != response.RecipientId)
await Clients.Group(response.RecipientId.ToString()).SendAsync("MessageRemoved", response);
}
else
{
await Clients.Group($"group_{response.RecipientId}").SendAsync("MessageRemoved", response);
}
}
private async Task NotifyClientsAboutEdit(MessageEditResponse response)
{
if (response.RecipientType == RecipientType.User)
{
await Clients.Group(response.EditorId.ToString()).SendAsync("MessageEdit", response);
if (response.EditorId != response.RecipientId)
await Clients.Group(response.RecipientId.ToString()).SendAsync("MessageEdit", response);
}
else
{
await Clients.Group($"group_{response.RecipientId}").SendAsync("MessageEdit", response);
}
}
// Logging helpers
private HubResult<T> LogAndError<T>(Guid userId, Guid targetId, string message, Exception ex)
{
_logger.LogError(ex, "{Message} from {UserId} to {TargetId}", message, userId, targetId);
return HubResult<T>.Error(ex?.Message ?? "Internal server error");
}
private HubResult<T> LogAndUnauthorized<T>(Exception ex, string msg, Guid userId, Guid targetId)
{
_logger.LogWarning(ex, "{Msg}: {UserId} -> {TargetId}", msg, userId, targetId);
return HubResult<T>.Unauthorized("You are not authorized to perform this action.");
}
private HubResult<T> LogAndNotFound<T>(Exception ex, string msg, Guid userId, Guid targetId)
{
_logger.LogWarning(ex, "{Msg}: {UserId} -> {TargetId}", msg, userId, targetId);
return HubResult<T>.NotFound("Message not found.");
}
#endregion
} }
+174
View File
@@ -0,0 +1,174 @@
using AutoMapper;
using Govor.API.Common.SignalR.Helpers;
using Govor.Application.Exceptions.FriendsService;
using Govor.Application.Interfaces.Friends;
using Govor.Contracts.DTOs;
using Govor.Contracts.Responses.SignalR;
using Microsoft.AspNetCore.SignalR;
namespace Govor.API.Hubs;
public class FriendsHub : Hub
{
private readonly ILogger<FriendsHub> _logger;
private readonly IFriendRequestCommandService _friendRequestService;
private readonly IHubUserAccessor _userAccessor;
private readonly IMapper _mapper;
public FriendsHub(
ILogger<FriendsHub> logger,
IFriendRequestCommandService friendRequestService,
IHubUserAccessor userAccessor,
IMapper mapper)
{
_logger = logger;
_friendRequestService = friendRequestService;
_userAccessor = userAccessor;
_mapper = mapper;
}
public override async Task OnConnectedAsync()
{
var userId = _userAccessor.GetUserId(Context);
if (userId == Guid.Empty)
{
_logger.LogWarning("User connected with invalid UserID claim.");
Context.Abort();
return;
}
await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString());
_logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId} and added to their group",
userId, Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var userId = _userAccessor.GetUserId(Context, true);
if (userId != Guid.Empty)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString());
_logger.LogInformation(
"User {UserId} disconnected with ConnectionId {ConnectionId} and removed from their group", userId,
Context.ConnectionId);
}
else if (exception != null)
{
_logger.LogWarning(exception,
"User disconnected with an exception and invalid UserId claim. ConnectionId: {ConnectionId}",
Context.ConnectionId);
}
else
{
_logger.LogInformation(
"User disconnected with no exception and invalid UserId claim. ConnectionId: {ConnectionId}",
Context.ConnectionId);
}
await base.OnDisconnectedAsync(exception);
}
public async Task<HubResult<object>> SendRequest(Guid targetUserId)
{
try
{
var userId = _userAccessor.GetUserId(Context);
var friendship = await _friendRequestService.SendAsync(userId, targetUserId);
await Clients.Group(targetUserId.ToString())
.SendAsync("FriendRequestReceived", _mapper.Map<FriendshipDto>(friendship));
_logger.LogInformation($"Friend request received for user {targetUserId} from {userId}.");
return HubResult<object>.Created();
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, ex.Message);
return HubResult<object>.BadRequest(ex.Message);
}
catch (RequestAlreadySentException ex)
{
_logger.LogWarning(ex, ex.Message);
return HubResult<object>.Conflict(ex.Message);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, ex.Message);
return HubResult<object>.Unauthorized(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return HubResult<object>.Error("Unexpected error! Please try later!");
}
}
public async Task<HubResult<object>> AcceptRequest(Guid friendshipId)
{
try
{
var userId = _userAccessor.GetUserId(Context);
var friendship = await _friendRequestService.AcceptAsync(friendshipId, userId);
await Clients.Group(userId.ToString())
.SendAsync("FriendRequestAccepted", _mapper.Map<FriendshipDto>(friendship));
await Clients.Group(friendship.RequesterId.ToString())
.SendAsync( "YourFriendRequestAccepted", _mapper.Map<UserDto>(friendship.Addressee));
_logger.LogInformation($"Friend request accepted for user {userId} from {userId}.");
return HubResult<object>.Ok();
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, ex.Message);
return HubResult<object>.BadRequest(ex.Message);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, ex.Message);
return HubResult<object>.Unauthorized(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return HubResult<object>.Error("Unexpected error! Please try later!");
}
}
public async Task<HubResult<object>> RejectRequest(Guid friendshipId)
{
try
{
var userId = _userAccessor.GetUserId(Context);
var friendship = await _friendRequestService.RejectAsync(friendshipId, userId);
var dto = _mapper.Map<FriendshipDto>(friendship);
await Clients.Group(userId.ToString())
.SendAsync("FriendRequestRejected", dto);
await Clients.Group(friendship.RequesterId.ToString())
.SendAsync("YourFriendRequestRejected", dto);
_logger.LogInformation($"Friend request rejected for user {userId} from {userId}.");
return HubResult<object>.Ok();
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, ex.Message);
return HubResult<object>.BadRequest(ex.Message);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, ex.Message);
return HubResult<object>.Unauthorized(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
return HubResult<object>.Error("Unexpected error! Please try later!");
}
}
}
@@ -2,7 +2,7 @@ using Microsoft.AspNetCore.SignalR;
namespace Govor.API.Hubs; namespace Govor.API.Hubs;
public class UsersHub : Hub public class GroupsHub : Hub
{ {
} }
+94
View File
@@ -0,0 +1,94 @@
using Govor.API.Common.SignalR.Helpers;
using Govor.Application.Interfaces.UserOnlineStatus;
using Govor.Core.Repositories.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.SignalR;
namespace Govor.API.Hubs;
[Authorize(Roles = "Admin, User")]
[Route("hubs/presence")]
public class PresenceHub : Hub
{
private readonly ILogger<PresenceHub> _logger;
private readonly IUserNotificationScopeService _scopeService;
private readonly IOnlineUserStore _onlineUserStore;
private readonly IHubUserAccessor _userAccessor;
private readonly IUsersRepository _users;
public PresenceHub(
ILogger<PresenceHub> logger,
IUserNotificationScopeService scopeService,
IOnlineUserStore onlineUserStore,
IHubUserAccessor userAccessor,
IUsersRepository users)
{
_logger = logger;
_users = users;
_scopeService = scopeService;
_onlineUserStore = onlineUserStore;
_userAccessor = userAccessor;
}
public override async Task OnConnectedAsync()
{
var userId = _userAccessor.GetUserId(Context);
if (userId == Guid.Empty || await _users.ExistsByIdAsync(userId) == false)
{
_logger.LogWarning("User connected with invalid UserId claim.");
Context.Abort();
return;
}
_onlineUserStore.SetOnlineUser(userId);
await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString());
var friends = await _scopeService.GetNotifiedUsers(userId);
foreach (var recipient in friends)
{
await Clients.Group(recipient.ToString())
.SendAsync("UserOnline", userId);
}
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
var userId = _userAccessor.GetUserId(Context, true);
if (userId != Guid.Empty)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString());
_logger.LogInformation(
"User {UserId} disconnected with ConnectionId {ConnectionId} and removed from their group", userId,
Context.ConnectionId);
}
else
{
_logger.LogInformation(
"User disconnected with no exception and invalid UserId claim. ConnectionId: {ConnectionId}",
Context.ConnectionId);
return;
}
_onlineUserStore.SetOfflineUser(userId);
// Updating was online
var user = await _users.FindByIdAsync(userId);
user.WasOnline = DateTime.UtcNow;
await _users.UpdateAsync(user);
var friends = await _scopeService.GetNotifiedUsers(userId);
foreach (var recipient in friends)
{
await Clients.Group(recipient.ToString())
.SendAsync("UserOffline", userId);
}
await base.OnDisconnectedAsync(exception);
}
}
+203
View File
@@ -0,0 +1,203 @@
using Govor.API.Common.SignalR.Helpers;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Friends;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Application.Interfaces.Medias;
using Govor.Contracts.Responses.SignalR;
using Govor.Core.Models;
using Govor.Core.Models.Users;
using Govor.Core.Repositories.Groups;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace Govor.API.Hubs;
[Authorize]
public class ProfileHub : Hub
{
private readonly IGroupsRepository _groupsRepository;
private readonly IFriendshipService _friendsService;
private readonly IProfileService _profileService;
private readonly IHubUserAccessor _userAccessor;
private readonly ILogger<ProfileHub> _logger;
private readonly IMediaService _mediaService;
public ProfileHub(
IGroupsRepository groupsRepository,
IFriendshipService friendsService,
IProfileService profileService,
IHubUserAccessor userAccessor,
ILogger<ProfileHub> logger)
{
_groupsRepository = groupsRepository;
_friendsService = friendsService;
_profileService = profileService;
_userAccessor = userAccessor;
_logger = logger;
}
public override async Task OnConnectedAsync()
{
var userId = _userAccessor.GetUserId(Context);
await Groups.AddToGroupAsync(Context.ConnectionId, $"user-{userId}");
// Friends
try
{
var friendships = await _friendsService.GetFriendsAsync(userId);
foreach (var friends in friendships)
await Groups.AddToGroupAsync(Context.ConnectionId, $"friends-{friends.Id}");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get friends for user {userId}", userId);
}
// Groups
try
{
var groups = await _groupsRepository.GetByUserIdAsync(userId);
foreach (var group in groups)
await Groups.AddToGroupAsync(Context.ConnectionId, $"group-{group.Id}");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get groups for user {userId}", userId);
}
_logger.LogInformation("User {UserId} connected with ConnectionId {ConnectionId}", userId, Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
var userId = _userAccessor.GetUserId(Context);
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user-{userId}");
// Friends
try
{
var friendships = await _friendsService.GetFriendsAsync(userId);
foreach (var friendship in friendships)
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"friends-{friendship.Id}");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to remove user {userId} from friend groups", userId);
}
// Groups
try
{
var groups = await _groupsRepository.GetByUserIdAsync(userId);
foreach (var group in groups)
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"group-{group.Id}");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to remove user {userId} from groups", userId);
}
await base.OnDisconnectedAsync(exception);
}
public async Task<HubResult<bool>> SetDescription(string description)
{
if(description.Length > 500)
return HubResult<bool>.Error("The description cannot be longer than 500 characters.");
var userId = _userAccessor.GetUserId(Context);
try
{
await _profileService.SetDescription(description, userId);
var payload = new { userId, description };
await NotifyFriendsAndGroupsAsync(userId, "DescriptionUpdated", payload);
return HubResult<bool>.Ok(true);
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while updating the user's {userId} descripton!", userId);
return HubResult<bool>.Error("An unaccounted error on the server!");
}
}
public async Task<HubResult<bool>> SetAvatar(Guid iconId)
{
var userId = _userAccessor.GetUserId(Context);
try
{
if (iconId == Guid.Empty)
return HubResult<bool>.BadRequest("IconId can't be empty!");
await _profileService.SetNewIcon(userId, iconId);
var payload = new { userId, iconId };
await NotifyFriendsAndGroupsAsync(userId, "AvatarUpdated", payload);
return HubResult<bool>.Ok(true);
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while updating the user's {userId} avatar {iconId}", userId, iconId);
return HubResult<bool>.Error("An unaccounted error on the server!");
}
}
private async Task NotifyFriendsAndGroupsAsync(Guid userId, string eventName, object payload)
{
var friendIds = Enumerable.Empty<User>();
var groups = Enumerable.Empty<ChatGroup>();
try
{
friendIds = await _friendsService.GetFriendsAsync(userId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load friend list for notifications. userId: {userId}", userId);
}
try
{
groups = await _groupsRepository.GetByUserIdAsync(userId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load group list for notifications. userId: {userId}", userId);
}
var groupIds = groups.Select(g => g.Id).ToList();
// Current user
await Clients.Group($"user-{userId}")
.SendAsync(eventName, payload);
// Friends
foreach (var friend in friendIds)
{
await Clients.Group($"friends-{friend.Id}")
.SendAsync(eventName, payload);
}
// Groups
foreach (var groupId in groupIds)
{
await Clients.Group($"group-{groupId}")
.SendAsync(eventName, payload);
}
_logger.LogInformation("Sent {EventName} for {UserId} to {Friends} friends and {Groups} groups",
eventName, userId, friendIds.Count(), groupIds.Count);
}
}
+30 -12
View File
@@ -1,7 +1,7 @@
using System.Text; using System.Text;
using Govor.API.Extensions; using Govor.API.Common.Extensions;
using Govor.API.Hubs; using Govor.API.Hubs;
using Govor.Application.Services; using Govor.Application.Services.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
@@ -11,23 +11,30 @@ var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration; var configuration = builder.Configuration;
var services = builder.Services; var services = builder.Services;
builder.AddLogger();// Serilog
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
builder.Services.AddCors(options => builder.Services.AddCors(options =>
{ {
options.AddPolicy("AllowFrontend", policy => options.AddPolicy("AllowFrontend", policy =>
{ {
policy.WithOrigins("http://localhost:3000", "https://localhost:3000") policy.WithOrigins(
"https://localhost:7155",
"http://localhost:7155",
"https://govor-team-govor-8ce1.twc1.net",
"http://govor-team-govor-8ce1.twc1.net"
)
.AllowAnyHeader() .AllowAnyHeader()
.AllowAnyMethod() .AllowAnyMethod()
.AllowCredentials(); .AllowCredentials();
}); });
}); });
builder.Services.Configure<JwtOption>(configuration.GetSection(nameof(JwtOption))); builder.Services.Configure<JwtAccessOption>(configuration.GetSection(nameof(JwtAccessOption)));
// Add services // Add services
builder.Services.AddSignalR(); builder.Services.AddSignalRConf();// signalR
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
@@ -39,7 +46,7 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
ValidateLifetime = true, ValidateLifetime = true,
ValidateIssuerSigningKey = true, ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey( IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["JwtOption:SecretKeу"]!)) Encoding.UTF8.GetBytes(builder.Configuration["JwtAccessOption:SecretKey"]!))
}; };
options.Events = new JwtBearerEvents options.Events = new JwtBearerEvents
{ {
@@ -65,6 +72,8 @@ builder.Services.AddServices();
builder.Services.AddRepositories(); builder.Services.AddRepositories();
builder.Services.AddValidators(); builder.Services.AddValidators();
builder.Services.AddOptionsConfiguration(configuration);
builder.Services.AddGovorDbContext(configuration); // GovorDbContext init builder.Services.AddGovorDbContext(configuration); // GovorDbContext init
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
@@ -94,18 +103,23 @@ builder.Services.AddSwaggerGen(options =>
}); });
}); });
//builder.Services.AddOpenApi(); //builder.Services.AddOpenApi();
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {
//app.MapOpenApi(); //app.MapOpenApi();
app.UseSwagger(); builder.WebHost.UseUrls("http://0.0.0.0:8080");
app.UseSwaggerUI();
} }
app.UseSwagger();
app.UseSwaggerUI();
app.UseCors("AllowFrontend");
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseRouting(); app.UseRouting();
@@ -114,9 +128,13 @@ app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); app.MapControllers();
app.MapHub<ChatsHub>("/api/chats");
app.MapSwagger().RequireAuthorization(); app.MapHub<ChatsHub>("/hubs/chats");
app.MapHub<FriendsHub>("/hubs/friends");
app.MapHub<ProfileHub>("/hubs/profiles");
app.MapSwagger()
.RequireAuthorization();
app.Map("/", () => "Not for browsers"); app.Map("/", () => "Not for browsers");
+3 -3
View File
@@ -5,7 +5,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": false, "launchBrowser": false,
"applicationUrl": "http://localhost:5041", "applicationUrl": "http://0.0.0.0:8080;",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
@@ -13,8 +13,8 @@
"https": { "https": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": false, "launchBrowser": true,
"applicationUrl": "https://localhost:7155;http://localhost:5041", "applicationUrl": "https://0.0.0.0:8080;",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
+11 -4
View File
@@ -6,11 +6,18 @@
} }
}, },
"ConnectionStrings": { "ConnectionStrings": {
"GovorDbContext": "Host=localhost;Port=5432;Database=DbGovor;Username=postgres;Password=stalcker;" "GovorDbContext": "Host=localhost;Port=5432;Database=GovorDb;Username=postgres;Password=stalcker;"
}, },
"UseMySql": false,
"AllowedHosts": "*", "AllowedHosts": "*",
"JwtOption": { "JwtAccessOption": {
"SecretKeу": "MY VERY SECRET KEY asdasdpafjhasofafpajsfj", "SecretKey": "Q89eY7zP7C4+TqLmHF4kw9xkF1E8Ru4Zpg+up9wFt9g=",
"Hours": "24" "Minutes": 10
},
"JwtRefreshOption": {
"RefreshTokenLifetimeDays": 30
},
"EncryptionOption": {
"Secret": "4r8B2j9kP5mX7nQwE2zY3A=="
} }
} }
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoFixture" Version="5.0.0-preview0012" />
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.4.0-beta.1" />
<PackageReference Include="NUnit.Analyzers" Version="4.4.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"/>
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Govor.Application\Govor.Application.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Validators\" />
</ItemGroup>
</Project>
@@ -1,11 +1,11 @@
using AutoFixture; using AutoFixture;
using Govor.API.Services.AdminsStuff.Interfaces; using Govor.Application.Infrastructure.AdminsStuff;
using Govor.Application.Interfaces.AdminsStuff; using Govor.Application.Interfaces;
using Govor.Core.Models; using Govor.Core.Models;
using Govor.Core.Repositories.Invaites; using Govor.Core.Repositories.Invaites;
using Moq; using Moq;
namespace Govor.API.Tests.UnitTests.Services.AdminStuff; namespace Govor.Application.Tests.Infrastructure.AdminsStuff;
[TestFixture] [TestFixture]
public class InvitationGeneratorTests public class InvitationGeneratorTests
@@ -0,0 +1,107 @@
using System.Security.Claims;
using Govor.Application.Infrastructure.Extensions;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Microsoft.AspNetCore.Http;
using Moq;
namespace Govor.Application.Tests.Infrastructure.Extensions;
[TestFixture]
public class CurrentUserServiceTests
{
private Mock<IHttpContextAccessor> _httpContextAccessorMock;
private ICurrentUserService _currentUserService;
[SetUp]
public void SetUp()
{
_httpContextAccessorMock = new Mock<IHttpContextAccessor>();
_currentUserService = new CurrentUserService(_httpContextAccessorMock.Object);
}
[Test]
public void GetCurrentUserId_ValidUserIdClaim_ReturnsGuid()
{
// Arrange
var userId = Guid.NewGuid();
var claims = new[] { new Claim("userId", userId.ToString()) };
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(x => x.User).Returns(principal);
_httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContextMock.Object);
// Act
var result = _currentUserService.GetCurrentUserId();
// Assert
Assert.That(result, Is.EqualTo(userId));
}
[Test]
public void GetCurrentUserId_NoHttpContext_ThrowsUnauthorizedAccessException()
{
// Arrange
_httpContextAccessorMock.Setup(x => x.HttpContext).Returns((HttpContext)null);
// Act & Assert
var ex = Assert.Throws<UnauthorizedAccessException>(() => _currentUserService.GetCurrentUserId());
Assert.That(ex.Message, Is.EqualTo("userID claim is missing or invalid"));
}
[Test]
public void GetCurrentUserId_NoUserIdClaim_ThrowsUnauthorizedAccessException()
{
// Arrange
var claims = new[] { new Claim("otherClaim", "value") };
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(x => x.User).Returns(principal);
_httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContextMock.Object);
// Act & Assert
var ex = Assert.Throws<UnauthorizedAccessException>(() => _currentUserService.GetCurrentUserId());
Assert.That(ex.Message, Is.EqualTo("userID claim is missing or invalid"));
}
[Test]
public void GetCurrentUserId_InvalidUserIdClaim_ThrowsUnauthorizedAccessException()
{
// Arrange
var claims = new[] { new Claim("userId", "invalid-guid") };
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(x => x.User).Returns(principal);
_httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContextMock.Object);
// Act & Assert
var ex = Assert.Throws<UnauthorizedAccessException>(() => _currentUserService.GetCurrentUserId());
Assert.That(ex.Message, Is.EqualTo("userID claim is missing or invalid"));
}
[Test]
public void GetCurrentUserId_EmptyUserIdClaim_ThrowsUnauthorizedAccessException()
{
// Arrange
var claims = new[] { new Claim("userId", "") };
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(x => x.User).Returns(principal);
_httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContextMock.Object);
// Act & Assert
var ex = Assert.Throws<UnauthorizedAccessException>(() => _currentUserService.GetCurrentUserId());
Assert.That(ex.Message, Is.EqualTo("userID claim is missing or invalid"));
}
}
@@ -0,0 +1,109 @@
using System.Security.Claims;
using Govor.Application.Infrastructure.Extensions;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Microsoft.AspNetCore.Http;
using Moq;
namespace Govor.Application.Tests.Infrastructure.Extensions;
[TestFixture]
[TestOf(typeof(CurrentUserSessionService))]
public class CurrentUserSessionServiceTests
{
private Mock<IHttpContextAccessor> _httpContextAccessorMock;
private ICurrentUserSessionService _sessionService;
[SetUp]
public void SetUp()
{
_httpContextAccessorMock = new Mock<IHttpContextAccessor>();
_sessionService = new CurrentUserSessionService(_httpContextAccessorMock.Object);
}
[Test]
public void GetCurrentSessionId_ValidSidClaim_ReturnsGuid()
{
// Arrange
var sid = Guid.NewGuid();
var claims = new[] { new Claim("sid", sid.ToString()) };
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(x => x.User).Returns(principal);
_httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContextMock.Object);
// Act
var result = _sessionService.GetUserSessionId();
// Assert
Assert.That(result, Is.EqualTo(sid));
}
[Test]
public void GetCurrentSessionId_NoHttpContext_ThrowsUnauthorizedAccessException()
{
// Arrange
_httpContextAccessorMock.Setup(x => x.HttpContext).Returns((HttpContext)null);
// Act & Assert
var ex = Assert.Throws<UnauthorizedAccessException>(() => _sessionService.GetUserSessionId());
Assert.That(ex.Message, Is.EqualTo("Session id (sid) claim is missing or invalid"));
}
[Test]
public void GetCurrentSessionId_NoSidlaim_ThrowsUnauthorizedAccessException()
{
// Arrange
var claims = new[] { new Claim("otherClaim", "value") };
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(x => x.User).Returns(principal);
_httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContextMock.Object);
// Act & Assert
var ex = Assert.Throws<UnauthorizedAccessException>(() => _sessionService.GetUserSessionId());
Assert.That(ex.Message, Is.EqualTo("Session id (sid) claim is missing or invalid"));
}
[Test]
public void GetUserSessionId_InvalidSidValueClaim_ThrowsUnauthorizedAccessException()
{
// Arrange
var claims = new[] { new Claim("sid", "invalid-guid") };
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(x => x.User).Returns(principal);
_httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContextMock.Object);
// Act & Assert
var ex = Assert.Throws<UnauthorizedAccessException>(() => _sessionService.GetUserSessionId());
Assert.That(ex.Message, Is.EqualTo("Session id (sid) claim is missing or invalid"));
}
[Test]
public void GetUserSessionId_EmptySidValueClaim_ThrowsUnauthorizedAccessException()
{
// Arrange
var claims = new[] { new Claim("sid", "") };
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(x => x.User).Returns(principal);
_httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContextMock.Object);
// Act & Assert
var ex = Assert.Throws<UnauthorizedAccessException>(() => _sessionService.GetUserSessionId());
Assert.That(ex.Message, Is.EqualTo("Session id (sid) claim is missing or invalid"));
}
}
@@ -1,7 +1,7 @@
using Govor.Application.Validators; // или другой namespace
using Govor.Application.Exceptions.AuthService; using Govor.Application.Exceptions.AuthService;
using Govor.Application.Infrastructure.Validators;
namespace Govor.API.Tests.UnitTests.Services.Validators; namespace Govor.Application.Tests.Infrastructure.Validators;
[TestFixture] [TestFixture]
public class UsernameValidatorTests public class UsernameValidatorTests
@@ -17,6 +17,7 @@ public class UsernameValidatorTests
[TestCase("Иван")] [TestCase("Иван")]
[TestCase("Алексей")] [TestCase("Алексей")]
[TestCase("Ёжик")] [TestCase("Ёжик")]
[TestCase("Иван123")] // содержит цифры
public void Validate_ValidUsernames_ShouldNotThrow(string username) public void Validate_ValidUsernames_ShouldNotThrow(string username)
{ {
Assert.DoesNotThrow(() => _validator.Validate(username)); Assert.DoesNotThrow(() => _validator.Validate(username));
@@ -24,7 +25,6 @@ public class UsernameValidatorTests
[TestCase("Ivan")] // не кириллица [TestCase("Ivan")] // не кириллица
[TestCase("123Иван")] // начинается не с буквы [TestCase("123Иван")] // начинается не с буквы
[TestCase("Иван123")] // содержит цифры
[TestCase("!@#$")] // спецсимволы [TestCase("!@#$")] // спецсимволы
[TestCase("")] // пусто [TestCase("")] // пусто
[TestCase("И")] // меньше минимума [TestCase("И")] // меньше минимума
@@ -2,14 +2,14 @@ using AutoFixture;
using Govor.Core.Infrastructure.Extensions; using Govor.Core.Infrastructure.Extensions;
using Govor.Core.Models; using Govor.Core.Models;
using Govor.Core.Repositories.Users; using Govor.Core.Repositories.Users;
using Govor.API.Services.Authentication.Interfaces;
using Govor.Application.Exceptions.AuthService; using Govor.Application.Exceptions.AuthService;
using Govor.Application.Interfaces.Authentication; using Govor.Application.Interfaces.Authentication;
using Govor.Application.Services; using Govor.Application.Services.Authentication;
using Govor.Core.Models.Users;
using Govor.Core.Repositories.Admins; using Govor.Core.Repositories.Admins;
using Moq; using Moq;
namespace Govor.API.Tests.UnitTests.Services.Authentication; namespace Govor.Application.Tests.Services.Authentication;
[TestFixture] [TestFixture]
public class AuthServiceTests public class AuthServiceTests
@@ -43,13 +43,13 @@ public class AuthServiceTests
_accountService = new AuthService( _accountService = new AuthService(
_usersRepositoryMock.Object, _usersRepositoryMock.Object,
_jwtServiceMock.Object,
_passwordHasherMock.Object, _passwordHasherMock.Object,
_adminsRepositoryMock.Object, _adminsRepositoryMock.Object,
_usernameValidatorMock.Object _usernameValidatorMock.Object
); );
} }
// Tests for Register action
[Test] [Test]
public void Given_ExistUser_When_Register_Should_Throw_UserAlreadyExistsException() public void Given_ExistUser_When_Register_Should_Throw_UserAlreadyExistsException()
{ {
@@ -0,0 +1,157 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using AutoFixture;
using Govor.Application.Interfaces.Authentication;
using Govor.Application.Services.Authentication;
using Govor.Core.Models.Users;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Moq;
namespace Govor.Application.Tests.Services.Authentication;
[TestFixture]
public class JwtServiceTests
{
private Fixture _fixture;
private Mock<IOptions<JwtAccessOption>> _jwtOptionsMock;
private Mock<IOptions<JwtRefreshOption>> _jwtRefreshOptionsMock;
private Mock<IInvitesService> _invitesServiceMock;
private IJwtService _jwtService;
private JwtAccessOption _testJwtAccessOptions;
private JwtRefreshOption _testJwtRefreshOptions;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_testJwtAccessOptions = new JwtAccessOption
{
SecretKey = "THIS_IS_A_TEST_SECRET_KEY_THAT_IS_LONG_ENOUGH_1234", // Ensure key size is sufficient for HMACSHA256
Minutes = 5
};
_testJwtRefreshOptions = new JwtRefreshOption()
{
RefreshTokenLifetimeDays = 30
};
_jwtOptionsMock = new Mock<IOptions<JwtAccessOption>>();
_jwtOptionsMock.Setup(o => o.Value).Returns(_testJwtAccessOptions);
_jwtRefreshOptionsMock = new Mock<IOptions<JwtRefreshOption>>();
_jwtRefreshOptionsMock.Setup(o => o.Value).Returns(_testJwtRefreshOptions);
_invitesServiceMock = new Mock<IInvitesService>();
_jwtService = new JwtService(
_jwtOptionsMock.Object,
_jwtRefreshOptionsMock.Object,
_invitesServiceMock.Object);
}
[Test]
public async Task GenerateJwtToken_ShouldReturnValidJwtString_WithCorrectClaims()
{
// Arrange
var user = _fixture.Create<User>();
var sessionId = Guid.NewGuid();
var expectedRole = "User";
_invitesServiceMock.Setup(s => s.GetRoleAsync(user)).ReturnsAsync(expectedRole);
// Act
var tokenString = await _jwtService.GenerateAccessTokenAsync(user, sessionId);
// Assert
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(tokenString);
var claims = jwt.Claims.ToDictionary(c => c.Type, c => c.Value);
Assert.That(claims["userId"], Is.EqualTo(user.Id.ToString()));
Assert.That(claims["sid"], Is.EqualTo(sessionId.ToString()));
Assert.That(claims[ClaimTypes.Role], Is.EqualTo(expectedRole));
Assert.That(jwt.ValidTo, Is.GreaterThan(DateTime.UtcNow));
}
[Test]
public async Task GenerateAccessTokenAsync_ShouldIncludeSessionIdAndRole()
{
// Arrange
var user = new User { Id = Guid.NewGuid(), Username = "TestUser" };
var sessionId = Guid.NewGuid();
var role = "Admin";
_invitesServiceMock.Setup(s => s.GetRoleAsync(user)).ReturnsAsync(role);
// Act
var token = await _jwtService.GenerateAccessTokenAsync(user, sessionId);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(token);
// Assert
var sidClaim = jwt.Claims.FirstOrDefault(c => c.Type == "sid");
var roleClaim = jwt.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role);
Assert.That(sidClaim?.Value, Is.EqualTo(sessionId.ToString()));
Assert.That(roleClaim?.Value, Is.EqualTo(role));
}
[Test]
public async Task GenerateRefreshTokenAsync_ReturnsValidRefreshToken()
{
// Arrange
var user = new User { Id = Guid.NewGuid() };
// Act
var token = await _jwtService.GenerateRefreshTokenAsync(user);
// Assert
Assert.That(token, Is.Not.Null);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(token);
Assert.That(user.Id.ToString(), Is.EqualTo(jwt.Claims.First(c => c.Type == "userId").Value));
Assert.That("refresh",Is.EqualTo(jwt.Claims.First(c => c.Type == "tokenType").Value));
}
[Test]
public async Task GetPrincipalFromExpiredToken_ReturnsValidClaimsPrincipal()
{
// Arrange
var userId = Guid.NewGuid();
var handler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_testJwtAccessOptions.SecretKey));
var now = DateTime.UtcNow;
var token = handler.CreateToken(new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim("userId", userId.ToString())
}),
NotBefore = now.AddSeconds(-10),
IssuedAt = now.AddSeconds(-10),
Expires = now.AddSeconds(-5),
SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
});
var expiredToken = handler.WriteToken(token);
// Act
var principal = _jwtService.GetPrincipalFromExpiredToken(expiredToken);
// Assert
Assert.That(principal, Is.Not.Null);
var claim = principal.FindFirst("userId");
Assert.That(claim, Is.Not.Null);
Assert.That(userId.ToString(), Is.EqualTo(claim.Value));
}
}
@@ -0,0 +1,51 @@
using AutoFixture;
using Govor.Application.Interfaces.Authentication;
using Govor.Application.Services.Authentication;
namespace Govor.Application.Tests.Services.Authentication;
[TestFixture]
public class JwtTokenHasherTests
{
private IJwtTokenHasher _jwtTokenHasher;
private Fixture _fixture;
[SetUp]
public void StarUp()
{
_fixture = new Fixture();
_jwtTokenHasher = new JwtTokenHasher();
}
[Test]
public void Given_Token_When_Hash_Then_Hash_And_Verify_Then_Result_Should_Be_True()
{
// Arrange
string token = _fixture.Create<string>();
// Act
string hash = _jwtTokenHasher.HashToken(token);
var result = _jwtTokenHasher.VerifyToken(token, hash);
// Assert
Assert.That(hash, Is.Not.EqualTo(token));
Assert.That(result, Is.True);
}
[Test]
public void Given_Token_NotToken_When_Hash_Should_Not_Be_True_Then_Result_Should_Be_False()
{
// Arrange
string token = _fixture.Create<string>();
string notToken = _fixture.Create<string>();
// Act
string hash = _jwtTokenHasher.HashToken(token);
var result = _jwtTokenHasher.VerifyToken(notToken, hash);
// Assert
Assert.That(result, Is.False);
}
}
@@ -0,0 +1,285 @@
using AutoFixture;
using Govor.Application.Exceptions.FriendsService;
using Govor.Application.Interfaces.Friends;
using Govor.Application.Services.Friends;
using Govor.Core.Models;
using Govor.Core.Repositories.Friendships;
using Govor.Core.Repositories.Users;
using Govor.Data.Repositories.Exceptions;
using Moq;
namespace Govor.Application.Tests.Services.Friends;
[TestFixture]
public class FriendRequestCommandServiceTests
{
private Fixture _fixture;
private Mock<IUsersRepository> _usersRepositoryMock;
private Mock<IFriendshipsRepository> _friendshipsRepositoryMock;
private IFriendRequestCommandService _service;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors
.OfType<ThrowingRecursionBehavior>()
.ToList()
.ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_usersRepositoryMock = new Mock<IUsersRepository>();
_friendshipsRepositoryMock = new Mock<IFriendshipsRepository>();
_service = new FriendRequestCommandService(_friendshipsRepositoryMock.Object);
}
// SendFriendRequestAsync
[Test]
public void SendFriendRequestAsync_SendingToSelf_ThrowsInvalidOperationException()
{
// Arrange
var userId = Guid.NewGuid();
// Act & Assert
var ex = Assert.ThrowsAsync<InvalidOperationException>(() =>
_service.SendAsync(userId, userId));
Assert.That(ex.Message, Is.EqualTo("Cannot send a request to self user"));
}
[Test]
public void SendFriendRequestAsync_RequestAlreadyExists_ThrowsRequestAlreadySentException()
{
// Arrange
var fromUserId = Guid.NewGuid();
var toUserId = Guid.NewGuid();
_friendshipsRepositoryMock
.Setup(repo => repo.Exist(fromUserId, toUserId))
.Returns(true);
// Act & Assert
Assert.ThrowsAsync<RequestAlreadySentException>(() =>
_service.SendAsync(fromUserId, toUserId));
}
[Test]
public async Task SendFriendRequestAsync_ValidRequest_CallsAddAsync()
{
// Arrange
var fromUserId = Guid.NewGuid();
var toUserId = Guid.NewGuid();
_friendshipsRepositoryMock
.Setup(repo => repo.Exist(fromUserId, toUserId))
.Returns(false);
// Act
await _service.SendAsync(fromUserId, toUserId);
// Assert
_friendshipsRepositoryMock.Verify(repo =>
repo.AddAsync(It.Is<Friendship>(f =>
f.RequesterId == fromUserId &&
f.AddresseeId == toUserId &&
f.Status == FriendshipStatus.Pending &&
f.Id != Guid.Empty
)),
Times.Once);
}
// AcceptFriendRequestAsync
[Test]
public async Task AcceptFriendRequestAsync_ValidRequest_CallsUpdateAsync()
{
var friendship = _fixture.Build<Friendship>()
.With(f => f.Status, FriendshipStatus.Pending)
.Create();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(friendship.Id))
.ReturnsAsync(friendship);
// Act: вызываем именно Accept, не Send
await _service.AcceptAsync(friendship.Id, friendship.AddresseeId);
// Assert
_friendshipsRepositoryMock.Verify(r => r.UpdateAsync(It.Is<Friendship>(f =>
f.Id == friendship.Id &&
f.RequesterId == friendship.RequesterId &&
f.AddresseeId == friendship.AddresseeId &&
f.Status == FriendshipStatus.Accepted
)), Times.Once);
}
[Test]
public void AcceptFriendRequestAsync_Throws_InvalidOperationException_IfStatusNotEqualPending()
{
var friendship = _fixture.Build<Friendship>()
.With(f => f.Status, FriendshipStatus.Accepted)
.Create();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(friendship.Id))
.ReturnsAsync(friendship);
// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(async () => await _service.AcceptAsync(friendship.Id, friendship.AddresseeId));
}
[Test]
public void AcceptFriendRequestAsync_Throws_UnauthorizedAccessException_IfCurrentUserIdIsNotAddressee()
{
// Arrange
var friendship = _fixture.Build<Friendship>()
.With(f => f.Status, FriendshipStatus.Pending)
.Create();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(friendship.Id))
.ReturnsAsync(friendship);
var wrongUserId = Guid.NewGuid(); // не совпадает с AddresseeId
// Act & Assert
Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
await _service.AcceptAsync(friendship.Id, wrongUserId));
}
[Test]
public void AcceptFriendRequestAsync_Throws_InvalidOperationException_IfFriendshipNotFound()
{
// Arrange
var requestId = Guid.NewGuid();
var userId = Guid.NewGuid();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(requestId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(requestId));
// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await _service.AcceptAsync(requestId, userId));
}
[Test]
public async Task AcceptFriendRequestAsync_ChangesStatusToAccepted()
{
var friendship = _fixture.Build<Friendship>()
.With(f => f.Status, FriendshipStatus.Pending)
.Create();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(friendship.Id))
.ReturnsAsync(friendship);
Friendship updatedFriendship = null;
_friendshipsRepositoryMock
.Setup(r => r.UpdateAsync(It.IsAny<Friendship>()))
.Callback<Friendship>(f => updatedFriendship = f)
.Returns(Task.CompletedTask);
await _service.AcceptAsync(friendship.Id, friendship.AddresseeId);
Assert.That(updatedFriendship.Status, Is.EqualTo(FriendshipStatus.Accepted));
}
// RejectFriend
[Test]
public async Task RejectFriendRequestAsync_ValidRequest_CallsRemoveAsync()
{
var friendship = _fixture.Build<Friendship>()
.With(f => f.Status, FriendshipStatus.Pending)
.Create();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(friendship.Id))
.ReturnsAsync(friendship);
await _service.RejectAsync(friendship.Id, friendship.AddresseeId);
// Assert
_friendshipsRepositoryMock.Verify(r => r.UpdateAsync(It.Is<Friendship>(f =>
f.Id == friendship.Id &&
f.RequesterId == friendship.RequesterId &&
f.AddresseeId == friendship.AddresseeId &&
f.Status == FriendshipStatus.Rejected
)), Times.Once);
}
[Test]
public void RejectFriendRequestAsync_Throws_InvalidOperationException_IfStatusNotEqualPendingOrReject()
{
var friendship = _fixture.Build<Friendship>()
.With(f => f.Status, FriendshipStatus.Accepted)
.Create();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(friendship.Id))
.ReturnsAsync(friendship);
// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(async () => await _service.RejectAsync(friendship.Id, friendship.AddresseeId));
}
[Test]
public void RejectFriendRequestAsync_Throws_UnauthorizedAccessException_IfCurrentUserIdIsNotAddressee()
{
// Arrange
var friendship = _fixture.Build<Friendship>()
.With(f => f.Status, FriendshipStatus.Pending)
.Create();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(friendship.Id))
.ReturnsAsync(friendship);
var wrongUserId = Guid.NewGuid(); // не совпадает с AddresseeId
// Act & Assert
Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
await _service.RejectAsync(friendship.Id, wrongUserId));
}
[Test]
public void RejectFriendRequestAsync_Throws_InvalidOperationException_IfFriendshipNotFound()
{
// Arrange
var requestId = Guid.NewGuid();
var userId = Guid.NewGuid();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(requestId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(requestId));
// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await _service.RejectAsync(requestId, userId));
}
[Test]
public async Task RejectFriendRequestAsync_ChangesStatusToAccepted()
{
var friendship = _fixture.Build<Friendship>()
.With(f => f.Status, FriendshipStatus.Pending)
.Create();
_friendshipsRepositoryMock
.Setup(r => r.GetByIdAsync(friendship.Id))
.ReturnsAsync(friendship);
Friendship updatedFriendship = null;
_friendshipsRepositoryMock
.Setup(r => r.UpdateAsync(It.IsAny<Friendship>()))
.Callback<Friendship>(f => updatedFriendship = f)
.Returns(Task.CompletedTask);
await _service.RejectAsync(friendship.Id, friendship.AddresseeId);
Assert.That(updatedFriendship.Status, Is.EqualTo(FriendshipStatus.Rejected));
}
}
@@ -0,0 +1,131 @@
using AutoFixture;
using Govor.Application.Interfaces.Friends;
using Govor.Application.Services.Friends;
using Govor.Core.Models;
using Govor.Core.Models.Users;
using Govor.Core.Repositories.Friendships;
using Govor.Core.Repositories.Users;
using Govor.Data.Repositories.Exceptions;
using Moq;
namespace Govor.Application.Tests.Services.Friends;
[TestFixture]
public class FriendRequestQueryServiceTests
{
private Fixture _fixture;
private Mock<IUsersRepository> _usersRepositoryMock;
private Mock<IFriendshipsRepository> _friendshipsRepositoryMock;
private IFriendRequestQueryService _service;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors
.OfType<ThrowingRecursionBehavior>()
.ToList()
.ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_usersRepositoryMock = new Mock<IUsersRepository>();
_friendshipsRepositoryMock = new Mock<IFriendshipsRepository>();
_service = new FriendRequestQueryService(_friendshipsRepositoryMock.Object);
}
// GetIncomingRequestsAsync
[Test]
public async Task GetIncomingRequestsAsync_ReturnsFriendships_IfFriendshipsExists()
{
// Arrange
var userId = Guid.NewGuid();
var friendships = _fixture.CreateMany<Friendship>().ToList();
var user = _fixture.Build<User>()
.With(u => u.Id, userId)
.With(u => u.ReceivedFriendRequests, friendships)
.Create();
friendships.ForEach(f =>
{
f.AddresseeId = userId;
f.Addressee = user;
f.Status = FriendshipStatus.Pending;
});
_friendshipsRepositoryMock.Setup(f => f.FindByUserIdAsync(userId))
.ReturnsAsync(friendships);
// Act
var result = await _service.GetIncomingAsync(userId);
// Assert
Assert.That(result.Count, Is.EqualTo(friendships.Count));
Assert.That(result.Select(u => u.Id), Is.EquivalentTo(friendships.Select(f => f.Id)));
}
[Test]
public void GetIncomingRequestsAsync_ThrowsInvalidOperationException_WhenUserNotFound()
{
// Arrange
var userId = Guid.NewGuid();
_friendshipsRepositoryMock
.Setup(r => r.FindByUserIdAsync(userId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(userId));
// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await _service.GetIncomingAsync(userId));
}
// GetResponsesAsync
[Test]
public async Task GetResponsesAsync_ReturnsFriendships_IfFriendshipsExists()
{
// Arrange
var userId = Guid.NewGuid();
var friendships = _fixture.CreateMany<Friendship>().ToList();
var user = _fixture.Build<User>()
.With(u => u.Id, userId)
.With(u => u.ReceivedFriendRequests, friendships)
.Create();
friendships.ForEach(f =>
{
f.RequesterId = userId;
f.Requester = user;
f.Status = FriendshipStatus.Rejected;
});
_friendshipsRepositoryMock.Setup(f => f.FindByUserIdAsync(userId))
.ReturnsAsync(friendships);
// Act
var result = await _service.GetResponsesAsync(userId);
// Assert
Assert.That(result.Count, Is.EqualTo(friendships.Count));
Assert.That(result.Select(u => u.Id), Is.EquivalentTo(friendships.Select(f => f.Id)));
}
[Test]
public void GetResponsesAsync_ThrowsInvalidOperationException_WhenUserNotFound()
{
// Arrange
var userId = Guid.NewGuid();
_friendshipsRepositoryMock
.Setup(r => r.FindByUserIdAsync(userId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(userId));
// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await _service.GetResponsesAsync(userId));
}
}
@@ -0,0 +1,162 @@
using AutoFixture;
using Govor.Application.Interfaces.Friends;
using Govor.Application.Services.Friends;
using Govor.Core.Models;
using Govor.Core.Models.Users;
using Govor.Core.Repositories.Friendships;
using Govor.Core.Repositories.Users;
using Govor.Data.Repositories.Exceptions;
using Moq;
namespace Govor.Application.Tests.Services.Friends;
[TestFixture]
public class FriendshipServiceTests
{
private Fixture _fixture;
private Mock<IUsersRepository> _usersRepositoryMock;
private Mock<IFriendshipsRepository> _friendshipsRepositoryMock;
IFriendshipService _service;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors
.OfType<ThrowingRecursionBehavior>()
.ToList()
.ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_usersRepositoryMock = new Mock<IUsersRepository>();
_friendshipsRepositoryMock = new Mock<IFriendshipsRepository>();
_service = new FriendshipService(_usersRepositoryMock.Object, _friendshipsRepositoryMock.Object);
}
// SearchUsersAsync
[Test]
public async Task SearchUsersAsync_ReturnsUsers_IfNotAlreadyFriends()
{
// Arrange
var userId = _fixture.Create<Guid>();
var user = _fixture.Create<User>();
user.Id = Guid.NewGuid();
_usersRepositoryMock
.Setup(u => u.SearchPotentialFriendsAsync(userId, It.IsAny<string>()))
.ReturnsAsync(new List<User> { user });
_friendshipsRepositoryMock
.Setup(f => f.FindByUserIdAsync(userId))
.ReturnsAsync(new List<Friendship>()); // No friends
// Act
var result = await _service.SearchUsersAsync("test", userId);
// Assert
Assert.That(result, Has.Count.EqualTo(1));
Assert.That(result[0].Id, Is.EqualTo(user.Id));
}
[Test]
public async Task SearchUsersAsync_Returns_EmptyList_IfThrowsNotFoundByKeyException_String_Guid()
{
// Arrange
_usersRepositoryMock
.Setup(u => u.SearchPotentialFriendsAsync(It.IsAny<Guid>(), It.IsAny<string>()))
.ThrowsAsync(new NotFoundByKeyException<(string,Guid)>(("test",Guid.NewGuid()),"test"));
// Act
var users = await _service.SearchUsersAsync("test", Guid.NewGuid());
// & Assert
Assert.That(users, Is.Empty);
}
[Test]
public async Task SearchUsersAsync_ReturnsUsersWithoutFriendships_IfThrowsNotFoundByKeyException_Guid()
{
// Arrange
var userId = _fixture.Create<Guid>();
var user = _fixture.Create<User>();
user.Id = Guid.NewGuid();
_usersRepositoryMock
.Setup(u => u.SearchPotentialFriendsAsync(userId, It.IsAny<string>()))
.ReturnsAsync(new List<User> { user });
_friendshipsRepositoryMock
.Setup(f => f.FindByUserIdAsync(userId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(userId, "test"));
// Act
var result = await _service.SearchUsersAsync("test", userId);
// Assert
Assert.That(result, Has.Count.EqualTo(1));
Assert.That(result[0].Id, Is.EqualTo(user.Id));
}
[Test]
public void SearchUsersAsync_Throws_UnauthorizedAccessException_IfThrowsSomeExceptionInUsersRepository()
{
// Arrange
_usersRepositoryMock
.Setup(u => u.SearchPotentialFriendsAsync(It.IsAny<Guid>(), It.IsAny<string>()))
.ThrowsAsync(new Exception("test"));
// Act & Assert
Assert.ThrowsAsync<UnauthorizedAccessException>(async () => await _service.SearchUsersAsync("test", Guid.NewGuid()));
}
[Test]
public void SearchUsersAsync_Throws_UnauthorizedAccessException_IfThrowsSomeExceptionInFriendshipsRepository()
{
// Arrange
_usersRepositoryMock
.Setup(u => u.SearchPotentialFriendsAsync(It.IsAny<Guid>(), "test"))
.ThrowsAsync(new Exception("test"));
// Act & Assert
Assert.ThrowsAsync<UnauthorizedAccessException>(
async () => await _service.SearchUsersAsync("test", Guid.NewGuid())
);
}
// GetFriendsAsync
[Test]
public async Task GetFriendsAsync_ReturnsUsers_IfUserExists()
{
// Arrange
var userId = Guid.NewGuid();
var friendships = _fixture.CreateMany<Friendship>().ToList();
friendships.ForEach(f =>
{
f.RequesterId = userId;
f.Status = FriendshipStatus.Accepted;
});
_friendshipsRepositoryMock.Setup(f => f.FindByUserIdAsync(userId))
.ReturnsAsync(friendships);
// Act
var result = await _service.GetFriendsAsync(userId);
// Assert
Assert.That(result.Count, Is.EqualTo(friendships.Count));
Assert.That(result, Is.EquivalentTo(friendships.Select(f => f.Addressee)));
}
[Test]
public void GetFriendsAsync_Throws_InvalidOperationException_IfUserNotExists()
{
// Arrange
_friendshipsRepositoryMock.Setup(f => f.FindByUserIdAsync(It.IsAny<Guid>()))
.ThrowsAsync(new NotFoundByKeyException<Guid>(Guid.NewGuid()));
// Act & Assert
Assert.ThrowsAsync<InvalidOperationException>(async () => await _service.GetFriendsAsync(Guid.NewGuid()));
}
}
@@ -0,0 +1,294 @@
using Govor.Application.Services.Medias;
using Govor.Core.Models;
using Govor.Core.Models.Messages;
using Govor.Data;
using Microsoft.EntityFrameworkCore;
namespace Govor.Application.Tests.Services.Medias;
[TestFixture]
public class AccesserToDownloadMediaServiceTests
{
private GovorDbContext _dbContext = null!;
private AccesserToDownloadMediaService _accesser = null!;
private Guid _userId;
private Guid _otherUserId;
private Guid _groupId;
private Guid _mediaFileId;
[SetUp]
public async Task SetUp()
{
var options = new DbContextOptionsBuilder<GovorDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_dbContext = new GovorDbContext(options);
_accesser = new AccesserToDownloadMediaService(_dbContext);
_userId = Guid.NewGuid();
_otherUserId = Guid.NewGuid();
_groupId = Guid.NewGuid();
_mediaFileId = Guid.NewGuid();
//
var message = new Message
{
Id = Guid.NewGuid(),
SenderId = _userId,
RecipientId = _otherUserId,
RecipientType = RecipientType.User
};
var media = new MediaFile
{
Id = _mediaFileId,
Url = "/media/test.png",
MineType = "image/png",
MediaType = MediaType.Image,
UploaderId = _userId,
DateCreated = DateTime.UtcNow,
OwnerType = MediaOwnerType.Message,
OwnerId = message.Id
};
var attachment = new MediaAttachments
{
Id = Guid.NewGuid(),
MediaFileId = _mediaFileId,
MessageId = message.Id,
Message = message,
MediaFile = media
};
await _dbContext.Messages.AddAsync(message);
await _dbContext.MediaFiles.AddAsync(media);
await _dbContext.MediaAttachments.AddAsync(attachment);
await _dbContext.SaveChangesAsync();
}
[Test]
public async Task HasAccessAsync_ReturnsTrue_ForSender()
{
var result = await _accesser.HasAccessAsync(_mediaFileId, _userId);
Assert.That(result, Is.True);
}
[Test]
public async Task HasAccessAsync_ReturnsTrue_ForRecipient()
{
var result = await _accesser.HasAccessAsync(_mediaFileId, _otherUserId);
Assert.That(result, Is.True);
}
[Test]
public async Task HasAccessAsync_ReturnsFalse_ForUnrelatedUser()
{
var unrelatedUserId = Guid.NewGuid();
var result = await _accesser.HasAccessAsync(_mediaFileId, unrelatedUserId);
Assert.That(result, Is.False);
}
[Test]
public async Task HasAccessAsync_ReturnsTrue_ForGroupMember()
{
var groupMediaId = Guid.NewGuid();
var groupMessage = new Message
{
Id = Guid.NewGuid(),
SenderId = _userId,
RecipientId = _groupId,
RecipientType = RecipientType.Group
};
var media = new MediaFile
{
Id = groupMediaId,
Url = "/media/group.png",
MineType = "image/png",
MediaType = MediaType.Image,
UploaderId = _userId,
DateCreated = DateTime.UtcNow,
OwnerType = MediaOwnerType.Message,
OwnerId = groupMessage.Id
};
var attachment = new MediaAttachments
{
Id = Guid.NewGuid(),
MediaFileId = groupMediaId,
MessageId = groupMessage.Id,
Message = groupMessage,
MediaFile = media
};
var membership = new GroupMembership
{
Id = Guid.NewGuid(),
GroupId = _groupId,
UserId = _otherUserId
};
await _dbContext.Messages.AddAsync(groupMessage);
await _dbContext.MediaFiles.AddAsync(media);
await _dbContext.MediaAttachments.AddAsync(attachment);
await _dbContext.GroupMemberships.AddAsync(membership);
await _dbContext.SaveChangesAsync();
var result = await _accesser.HasAccessAsync(groupMediaId, _otherUserId);
Assert.That(result, Is.True);
}
[Test]
public async Task HasAccessAsync_ReturnsFalse_IfMediaNotAttached()
{
var result = await _accesser.HasAccessAsync(Guid.NewGuid(), _userId);
Assert.That(result, Is.False);
}
[Test]
public async Task HasAccessAsync_ReturnsTrue_ForUserAvatar()
{
var avatarMedia = new MediaFile
{
Id = Guid.NewGuid(),
Url = "/media/avatar.png",
MineType = "image/png",
MediaType = MediaType.Image,
UploaderId = _userId,
OwnerType = MediaOwnerType.Avatar,
OwnerId = _userId,
DateCreated = DateTime.UtcNow
};
await _dbContext.MediaFiles.AddAsync(avatarMedia);
await _dbContext.SaveChangesAsync();
var result = await _accesser.HasAccessAsync(avatarMedia.Id, _userId);
Assert.That(result, Is.True);
}
[Test]
public async Task HasAccessAsync_ReturnsTrue_ForOtherUserAvatar()
{
var avatarMedia = new MediaFile
{
Id = Guid.NewGuid(),
Url = "/media/avatar2.png",
MineType = "image/png",
MediaType = MediaType.Image,
UploaderId = _otherUserId,
OwnerType = MediaOwnerType.Avatar,
OwnerId = _otherUserId,
DateCreated = DateTime.UtcNow
};
await _dbContext.MediaFiles.AddAsync(avatarMedia);
await _dbContext.SaveChangesAsync();
var result = await _accesser.HasAccessAsync(avatarMedia.Id, _userId);
Assert.That(result, Is.True);
}
[Test]
public async Task HasAccessAsync_ReturnsTrue_ForGroupAvatarMember()
{
var groupAvatar = new MediaFile
{
Id = Guid.NewGuid(),
Url = "/media/group_avatar.png",
MineType = "image/png",
MediaType = MediaType.Image,
UploaderId = _userId,
OwnerType = MediaOwnerType.GroupAvatar,
OwnerId = _groupId,
DateCreated = DateTime.UtcNow
};
var membership = new GroupMembership
{
Id = Guid.NewGuid(),
GroupId = _groupId,
UserId = _otherUserId
};
await _dbContext.MediaFiles.AddAsync(groupAvatar);
await _dbContext.GroupMemberships.AddAsync(membership);
await _dbContext.SaveChangesAsync();
var result = await _accesser.HasAccessAsync(groupAvatar.Id, _otherUserId);
Assert.That(result, Is.True);
}
[Test]
public async Task HasAccessAsync_ReturnsFalse_ForGroupAvatarNonMember()
{
var groupAvatar = new MediaFile
{
Id = Guid.NewGuid(),
Url = "/media/group_avatar2.png",
MineType = "image/png",
MediaType = MediaType.Image,
UploaderId = _userId,
OwnerType = MediaOwnerType.GroupAvatar,
OwnerId = _groupId,
DateCreated = DateTime.UtcNow
};
await _dbContext.MediaFiles.AddAsync(groupAvatar);
await _dbContext.SaveChangesAsync();
var unrelatedUserId = Guid.NewGuid();
var result = await _accesser.HasAccessAsync(groupAvatar.Id, unrelatedUserId);
Assert.That(result, Is.False);
}
[Test]
public async Task HasAccessAsync_ReturnsTrue_ForSystemMedia()
{
var systemMedia = new MediaFile
{
Id = Guid.NewGuid(),
Url = "/media/system.png",
MineType = "image/png",
MediaType = MediaType.Image,
UploaderId = _userId,
OwnerType = MediaOwnerType.System,
DateCreated = DateTime.UtcNow
};
await _dbContext.MediaFiles.AddAsync(systemMedia);
await _dbContext.SaveChangesAsync();
var result = await _accesser.HasAccessAsync(systemMedia.Id, Guid.NewGuid());
Assert.That(result, Is.True);
}
[Test]
public async Task HasAccessAsync_ReturnsFalse_ForUnknownOwnerType()
{
var unknownMedia = new MediaFile
{
Id = Guid.NewGuid(),
Url = "/media/unknown.png",
MineType = "image/png",
MediaType = MediaType.Image,
UploaderId = _userId,
OwnerType = (MediaOwnerType)999, //
DateCreated = DateTime.UtcNow
};
await _dbContext.MediaFiles.AddAsync(unknownMedia);
await _dbContext.SaveChangesAsync();
var result = await _accesser.HasAccessAsync(unknownMedia.Id, _userId);
Assert.That(result, Is.False);
}
[TearDown]
public void TearDown()
{
_dbContext.Dispose();
}
}
@@ -0,0 +1,376 @@
using Govor.Application.Exceptions.VerifyFriendship;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Medias;
using Govor.Application.Interfaces.Messages.Parameters;
using Govor.Application.Services.Messages;
using Govor.Core.Models;
using Govor.Core.Models.Messages;
using Govor.Core.Repositories.Groups;
using Govor.Core.Repositories.Messages;
using Govor.Core.Repositories.PrivateChats;
using Govor.Core.Repositories.Users;
using Govor.Data.Repositories.Exceptions;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.Application.Tests.Services.Messages;
[TestFixture]
public class MessageCommandServiceTests
{
private Mock<IMessagesRepository> _mockMessagesRepo;
private Mock<IUsersRepository> _mockUsersRepo;
private Mock<IGroupsRepository> _mockGroupsRepo;
private Mock<IVerifyFriendship> _mockVerifyFriendship;
private Mock<IPrivateChatsRepository> _mockPrivateChats;
private Mock<IMediaService> _mockMediaService;
private Mock<ILogger<MessageCommandService>> _mockLogger;
private MessageCommandService _messageService;
[SetUp]
public void SetUp()
{
_mockMessagesRepo = new Mock<IMessagesRepository>();
_mockUsersRepo = new Mock<IUsersRepository>();
_mockGroupsRepo = new Mock<IGroupsRepository>();
_mockVerifyFriendship = new Mock<IVerifyFriendship>();
_mockPrivateChats = new Mock<IPrivateChatsRepository>();
_mockMediaService = new Mock<IMediaService>();
_mockLogger = new Mock<ILogger<MessageCommandService>>();
_messageService = new MessageCommandService(
_mockMessagesRepo.Object,
_mockUsersRepo.Object,
_mockGroupsRepo.Object,
_mockVerifyFriendship.Object,
_mockPrivateChats.Object,
_mockMediaService.Object,
_mockLogger.Object);
}
// Test for SendMessageAsync action
[Test]
public async Task SendMessageAsync_ToUser_Success()
{
// Arrange
var senderId = Guid.NewGuid();
var recipientId = Guid.NewGuid();
var sendMessageParams = new SendMessage("Hello",
null,
recipientId,
RecipientType.User,
senderId,
DateTime.UtcNow,
new List<SendMedia>());
_mockUsersRepo.Setup(r => r.ExistsByIdAsync(recipientId)).ReturnsAsync(true);
_mockVerifyFriendship.Setup(v => v.VerifyAsync(senderId, recipientId)).Returns(Task.CompletedTask);
_mockMessagesRepo.Setup(r => r.AddAsync(It.IsAny<Message>())).Returns(Task.CompletedTask);
_mockPrivateChats.Setup(c => c.Exist(senderId, recipientId)).Returns(true);
_mockPrivateChats.Setup(c => c.GetByMembersAsync(senderId, recipientId)).ReturnsAsync(new PrivateChat(){Id = recipientId});
// Act
var result = await _messageService.SendMessageAsync(sendMessageParams);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Exception, Is.Null);
_mockMessagesRepo.Verify(r => r.AddAsync(It.Is<Message>(m =>
m.SenderId == senderId &&
m.RecipientId == recipientId &&
m.RecipientType == RecipientType.User &&
m.EncryptedContent == "Hello")), Times.Once);
}
[Test]
public async Task SendMessageAsync_ToUser_When_AttachMediaThrowsException_ReturnsFailure()
{
// Arrange
var senderId = Guid.NewGuid();
var recipientId = Guid.NewGuid();
var sendMediaId = Guid.NewGuid();
var sendMessageParams = new SendMessage(
"Hello",
null,
recipientId,
RecipientType.User,
senderId,
DateTime.UtcNow,
new List<SendMedia> { new SendMedia(sendMediaId, string.Empty) });
_mockUsersRepo.Setup(r => r.ExistsByIdAsync(recipientId)).ReturnsAsync(true);
_mockVerifyFriendship.Setup(v => v.VerifyAsync(senderId, recipientId)).Returns(Task.CompletedTask);
_mockMessagesRepo.Setup(r => r.AddAsync(It.IsAny<Message>())).Returns(Task.CompletedTask);
_mockPrivateChats.Setup(c => c.Exist(senderId, recipientId)).Returns(true);
_mockPrivateChats.Setup(c => c.GetByMembersAsync(senderId, recipientId))
.ReturnsAsync(new PrivateChat { Id = recipientId });
// !
_mockMediaService
.Setup(m => m.AttachToMessageAsync(sendMediaId, It.IsAny<Guid>()))
.ThrowsAsync(new Exception("Unexpected DB error"));
// Act
var result = await _messageService.SendMessageAsync(sendMessageParams);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Exception, Is.Not.Null);
Assert.That(result.Exception.Message, Is.EqualTo("Unexpected DB error"));
// Проверяем, что сообщение не было сохранено
_mockMessagesRepo.Verify(r => r.AddAsync(It.IsAny<Message>()), Times.Never);
// Проверяем, что метод прикрепления медиа вызывался
_mockMediaService.Verify(m => m.AttachToMessageAsync(sendMediaId, It.IsAny<Guid>()), Times.Once);
}
[Test]
public async Task SendMessageAsync_ToGroup_Success()
{
// Arrange
var senderId = Guid.NewGuid();
var groupId = Guid.NewGuid();
var sendMessageParams = new SendMessage("Hello Group",
null,
groupId,
RecipientType.Group,
senderId,
DateTime.UtcNow,
new List<SendMedia>());
_mockGroupsRepo.Setup(r => r.Exist(groupId)).Returns(true);
_mockGroupsRepo.Setup(r => r.IsUserMemberOfGroupAsync(senderId, groupId)).ReturnsAsync(true);
_mockMessagesRepo.Setup(r => r.AddAsync(It.IsAny<Message>())).Returns(Task.CompletedTask);
// Act
var result = await _messageService.SendMessageAsync(sendMessageParams);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Exception, Is.Null);
_mockMessagesRepo.Verify(r => r.AddAsync(It.Is<Message>(m =>
m.RecipientId == groupId &&
m.RecipientType == RecipientType.Group)), Times.Once);
}
[Test]
public async Task SendMessageAsync_ToUser_RecipientNotFound_ReturnsFailure()
{
// Arrange
var senderId = Guid.NewGuid();
var recipientId = Guid.NewGuid();
var sendMessageParams = new SendMessage("Hello",
null,
recipientId,
RecipientType.User,
senderId,
DateTime.UtcNow,
new List<SendMedia>());
_mockUsersRepo.Setup(r => r.ExistsByIdAsync(recipientId)).ReturnsAsync(false);
// Act
var result = await _messageService.SendMessageAsync(sendMessageParams);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Exception, Is.Not.Null);
Assert.That(result.Exception,Is.TypeOf<KeyNotFoundException>());
Assert.That(result.Message, Is.Null);
}
[Test]
public async Task SendMessageAsync_ToUser_FriendshipVerificationFails_ReturnsFailure()
{
// Arrange
var senderId = Guid.NewGuid();
var recipientId = Guid.NewGuid();
var sendMessageParams = new SendMessage("Hello",
null,
recipientId,
RecipientType.User,
senderId,
DateTime.UtcNow,
new List<SendMedia>()
);
_mockUsersRepo.Setup(r => r.ExistsByIdAsync(recipientId)).ReturnsAsync(true);
_mockVerifyFriendship.Setup(v => v.VerifyAsync(senderId, recipientId)).ThrowsAsync(new FriendshipException("Not friends"));
// Act
var result = await _messageService.SendMessageAsync(sendMessageParams);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Exception, Is.Not.Null);
Assert.That(result.Exception,Is.TypeOf<FriendshipException>());
Assert.That(result.Message, Is.Null);
}
// Test for EditMessageAsync action
[Test]
public async Task EditMessageAsync_Success()
{
// Arrange
var editorId = Guid.NewGuid();
var messageId = Guid.NewGuid();
var originalMessage = new Message
{
Id = messageId,
SenderId = editorId,
EncryptedContent = "Old",
RecipientId = Guid.NewGuid(),
RecipientType = RecipientType.User
};
var editParams = new EditMessage(editorId, messageId, "New Content", DateTime.UtcNow);
_mockMessagesRepo.Setup(r => r.FindByIdAsync(messageId)).ReturnsAsync(originalMessage);
_mockMessagesRepo.Setup(r => r.UpdateAsync(It.IsAny<Message>())).Returns(Task.CompletedTask);
// Act
var result = await _messageService.EditMessageAsync(editParams);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.OriginalMessage, Is.Not.Null);
Assert.That(messageId, Is.EqualTo(result.OriginalMessage!.Id));
_mockMessagesRepo.Verify(r => r.UpdateAsync(It.Is<Message>(m =>
m.Id == messageId &&
m.EncryptedContent == "New Content" &&
m.IsEdited == true &&
m.EditedAt == editParams.EditedAt)), Times.Once);
}
[Test]
public async Task EditMessageAsync_MessageNotFound_ReturnsFailure()
{
// Arrange
var editorId = Guid.NewGuid();
var messageId = Guid.NewGuid();
var editParams = new EditMessage(editorId, messageId, "New Content", DateTime.UtcNow);
_mockMessagesRepo.Setup(r => r.FindByIdAsync(messageId)).
ThrowsAsync(new NotFoundByKeyException<Guid>(messageId));
// Act
var result = await _messageService.EditMessageAsync(editParams);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Exception, Is.Not.Null);
Assert.That(result.Exception,Is.TypeOf<NotFoundByKeyException<Guid>>());
Assert.That(result.OriginalMessage, Is.Null);
}
[Test]
public async Task EditMessageAsync_NotSender_ReturnsFailure()
{
// Arrange
var editorId = Guid.NewGuid();
var senderId = Guid.NewGuid(); // Different from editorId
var messageId = Guid.NewGuid();
var originalMessage = new Message { Id = messageId, SenderId = senderId, EncryptedContent = "Old" };
var editParams = new EditMessage(editorId, messageId, "New Content", DateTime.UtcNow);
_mockMessagesRepo.Setup(r => r.FindByIdAsync(messageId)).ReturnsAsync(originalMessage);
// Act
var result = await _messageService.EditMessageAsync(editParams);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Exception, Is.Not.Null);
Assert.That(result.Exception,Is.TypeOf<UnauthorizedAccessException>());
Assert.That(result.OriginalMessage, Is.Null);
}
// Test for DeleteMessageAsync action
[Test]
public async Task DeleteMessageAsync_Success()
{
// Arrange
var deleterId = Guid.NewGuid();
var messageId = Guid.NewGuid();
var originalMessage = new Message { Id = messageId, SenderId = deleterId, RecipientId = Guid.NewGuid(), RecipientType = RecipientType.User };
var deleteParams = new DeleteMessage(deleterId, messageId);
_mockMessagesRepo.Setup(r => r.FindByIdAsync(messageId)).ReturnsAsync(originalMessage);
_mockMessagesRepo.Setup(r => r.RemoveAsync(messageId)).Returns(Task.CompletedTask);
// Act
var result = await _messageService.DeleteMessageAsync(deleteParams);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.OriginalMessage, Is.Not.Null);
Assert.That(messageId, Is.EqualTo(result.OriginalMessage!.Id));
_mockMessagesRepo.Verify(r => r.RemoveAsync(messageId), Times.Once);
}
[Test]
public async Task DeleteMessageAsync_MessageNotFound_ReturnsFailure()
{
// Arrange
var deleterId = Guid.NewGuid();
var messageId = Guid.NewGuid();
var deleteParams = new DeleteMessage(deleterId, messageId);
_mockMessagesRepo.Setup(r => r.FindByIdAsync(messageId)).
ThrowsAsync(new NotFoundByKeyException<Guid>(messageId));
// Act
var result = await _messageService.DeleteMessageAsync(deleteParams);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Exception, Is.Not.Null);
Assert.That(result.Exception,Is.TypeOf<KeyNotFoundException>());
Assert.That(result.OriginalMessage, Is.Null);
}
[Test]
public async Task DeleteMessageAsync_NotSender_ReturnsFailure()
{
// Arrange
var deleterId = Guid.NewGuid();
var senderId = Guid.NewGuid(); // Different
var messageId = Guid.NewGuid();
var originalMessage = new Message { Id = messageId, SenderId = senderId };
var deleteParams = new DeleteMessage(deleterId, messageId);
_mockMessagesRepo.Setup(r => r.FindByIdAsync(messageId)).ReturnsAsync(originalMessage);
// Act
var result = await _messageService.DeleteMessageAsync(deleteParams);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Exception, Is.Not.Null);
Assert.That(result.Exception,Is.TypeOf<UnauthorizedAccessException>());
Assert.That(result.OriginalMessage, Is.Null);
}
}
@@ -0,0 +1,168 @@
using Govor.Application.Interfaces;
using Govor.Application.Services.Messages;
using Govor.Core.Models;
using Govor.Core.Models.Messages;
using Govor.Core.Repositories.Groups;
using Govor.Core.Repositories.PrivateChats;
using Govor.Data;
using Microsoft.EntityFrameworkCore;
using Moq;
namespace Govor.Application.Tests.Services.Messages;
[TestFixture]
public class MessagesLoaderTests
{
private IMessagesLoader _loader;
private Mock<IPrivateChatsRepository> _privateChatsRepoMock;
private Mock<IGroupsRepository> _groupsRepoMock;
private GovorDbContext _dbContext;
private Guid _currentUserId = Guid.NewGuid();
private Guid _otherUserId = Guid.NewGuid();
private Guid _groupChatId = Guid.NewGuid();
[SetUp]
public void Setup()
{
_privateChatsRepoMock = new Mock<IPrivateChatsRepository>();
_groupsRepoMock = new Mock<IGroupsRepository>();
var options = new DbContextOptionsBuilder<GovorDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_dbContext = new GovorDbContext(options);
_loader = new MessagesLoader(_groupsRepoMock.Object, _privateChatsRepoMock.Object, _dbContext);
}
// Tests for LoadLastMessagesInUserChat action
[Test]
public async Task LoadLastMessagesInUserChat_ReturnsMessages_WhenChatExists()
{
// Arrange
var chatId = Guid.NewGuid();
_privateChatsRepoMock.Setup(r => r.Exist(_currentUserId, _otherUserId)).Returns(true);
_privateChatsRepoMock.Setup(r => r.GetByMembersAsync(_currentUserId, _otherUserId))
.ReturnsAsync(new PrivateChat() { Id = chatId });
var messages = new List<Message>
{
new() { Id = Guid.NewGuid(), RecipientId = chatId, RecipientType = RecipientType.User }
};
await _dbContext.Messages.AddRangeAsync(messages);
await _dbContext.SaveChangesAsync();
// Act
var result = await _loader.LoadMessagesInUserChat(_currentUserId, _otherUserId, null);
// Assert
Assert.That(result, Has.Count.EqualTo(1));
}
[Test]
public async Task LoadLastMessagesInUserChat_ReturnsEmpty_WhenNoMessages()
{
// Arrange
_privateChatsRepoMock.Setup(r => r.Exist(_currentUserId, _otherUserId)).Returns(true);
_privateChatsRepoMock.Setup(r => r.GetByMembersAsync(_currentUserId, _otherUserId))
.ReturnsAsync(new PrivateChat { Id = Guid.NewGuid() });
// Act
var result = await _loader.LoadMessagesInUserChat(_currentUserId, _otherUserId, null);
// Assert
Assert.That(result, Is.Empty);
}
[Test]
public void LoadLastMessagesInUserChat_Throws_WhenUserIdIsEmpty()
{
// Act & Assert
var ex = Assert.ThrowsAsync<ArgumentException>(async () =>
await _loader.LoadMessagesInUserChat(Guid.Empty, _currentUserId, null));
Assert.That(ex.Message, Does.Contain("User id cannot be empty"));
}
[Test]
public void LoadLastMessagesInUserChat_Throws_WhenChatNotExists()
{
// Arrange
_privateChatsRepoMock.Setup(r => r.Exist(It.IsAny<Guid>(), It.IsAny<Guid>())).Returns(false);
// Act & Assert
var ex = Assert.ThrowsAsync<InvalidOperationException>(async () =>
await _loader.LoadMessagesInUserChat(_currentUserId, _otherUserId, null));
Assert.That(ex.Message, Is.EqualTo("Private chat not found"));
}
// Tests for LoadLastMessagesInChatGroup action
[Test]
public async Task LoadLastMessagesInChatGroup_ReturnsMessages_WhenUserIsMember()
{
// Arrange
_groupsRepoMock.Setup(g => g.IsUserMemberOfGroupAsync(_currentUserId, _groupChatId))
.ReturnsAsync(true);
var messages = new List<Message>
{
new() { Id = Guid.NewGuid(), RecipientId = _groupChatId, RecipientType = RecipientType.Group }
};
await _dbContext.Messages.AddRangeAsync(messages);
await _dbContext.SaveChangesAsync();
// Act
var result = await _loader.LoadMessagesInChatGroup(_groupChatId, _currentUserId, null);
// Assert
Assert.That(result, Has.Count.EqualTo(1));
}
[Test]
public void LoadLastMessagesInChatGroup_Throws_WhenChatIdEmpty()
{
// Act & Assert
var ex = Assert.ThrowsAsync<ArgumentException>(async () =>
await _loader.LoadMessagesInChatGroup(Guid.Empty, _currentUserId, null));
Assert.That(ex.Message, Is.EqualTo("Chat id cannot be empty"));
}
[Test]
public void LoadLastMessagesInChatGroup_Throws_WhenUserNotInGroup()
{
// Arrange
_groupsRepoMock.Setup(g => g.IsUserMemberOfGroupAsync(_currentUserId, _groupChatId))
.ReturnsAsync(false);
// Act & Assert
var ex = Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
await _loader.LoadMessagesInChatGroup(_groupChatId, _currentUserId, null));
Assert.That(ex.Message, Is.EqualTo("You are not a member of this group."));
}
[Test]
public async Task LoadLastMessagesInChatGroup_ReturnsEmpty_WhenNoMessages()
{
// Arrange
_groupsRepoMock.Setup(g => g.IsUserMemberOfGroupAsync(_currentUserId, _groupChatId))
.ReturnsAsync(true);
// Act
var result = await _loader.LoadMessagesInChatGroup(_groupChatId, _currentUserId, null);
// Assert
Assert.That(result, Is.Empty);
}
[TearDown]
public void TearDown()
{
_dbContext.Dispose();
}
}
@@ -1,8 +1,8 @@
using AutoFixture; using AutoFixture;
using Govor.Application.Services; using Govor.Application.Services.Authentication;
using Govor.Core.Infrastructure.Extensions; using Govor.Core.Infrastructure.Extensions;
namespace Govor.API.Tests.UnitTests.Services; namespace Govor.Application.Tests.Services;
[TestFixture] [TestFixture]
public class PasswordHasherTests public class PasswordHasherTests
@@ -0,0 +1,65 @@
using Govor.Application.Services;
using Govor.Core.Models.Users;
using Govor.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
namespace Govor.Application.Tests.Services;
[TestFixture]
public class PingHandlerServiceTests
{
private GovorDbContext _dbContext = null!;
private IMemoryCache _memoryCache = null!;
private PingHandlerService _service = null!;
private Guid _userId;
[SetUp]
public void SetUp()
{
var options = new DbContextOptionsBuilder<GovorDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_dbContext = new GovorDbContext(options);
_memoryCache = new MemoryCache(new MemoryCacheOptions());
_service = new PingHandlerService(_dbContext, _memoryCache);
_userId = Guid.NewGuid();
_dbContext.Users.Add(new User
{
Id = _userId,
Username = "TestUser",
WasOnline = DateTime.UtcNow.AddHours(-1),
Description = "Test description",
PasswordHash = "hashed_password_here"
});
_dbContext.SaveChanges();
}
[Test]
public async Task Ping_DoesNotUpdate_WhenPingTooRecent()
{
// Arrange
var initial = DateTime.UtcNow.AddMinutes(-1);
_memoryCache.Set($"LastPing_{_userId}", DateTime.UtcNow);
var user = await _dbContext.Users.FirstAsync(u => u.Id == _userId);
var originalTime = user.WasOnline;
// Act
await _service.Ping(_userId);
var updatedUser = await _dbContext.Users.FirstAsync(u => u.Id == _userId);
// Assert
Assert.That(updatedUser.WasOnline, Is.EqualTo(originalTime));
}
[TearDown]
public void TearDown()
{
_dbContext?.Dispose();
_memoryCache?.Dispose();
}
}
@@ -0,0 +1,144 @@
using Govor.Application.Services;
using Govor.Core.Models.Users;
using Govor.Core.Repositories.Users;
using Govor.Data.Repositories.Exceptions;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.Application.Tests.Services;
[TestFixture]
public class ProfileServiceTests
{
private Mock<IUsersRepository> _mockUsersRepo = null!;
private Mock<ILogger<ProfileService>> _mockLogger = null!;
private ProfileService _service = null!;
private User _testUser = null!;
[SetUp]
public void SetUp()
{
_mockUsersRepo = new Mock<IUsersRepository>();
_mockLogger = new Mock<ILogger<ProfileService>>();
_service = new ProfileService(_mockUsersRepo.Object, _mockLogger.Object);
_testUser = new User
{
Id = Guid.NewGuid(),
Username = "testuser",
Description = "old description",
IconId = Guid.NewGuid()
};
}
// ---------- GetUserProfileAsync ----------
[Test]
public async Task GetUserProfileAsync_ReturnsCorrectData()
{
// Arrange
_mockUsersRepo.Setup(r => r.FindByIdAsync(_testUser.Id))
.ReturnsAsync(_testUser);
// Act
var result = await _service.GetUserProfileAsync(_testUser.Id);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Id, Is.EqualTo(_testUser.Id));
Assert.That(result.Username, Is.EqualTo("testuser"));
Assert.That(result.Description, Is.EqualTo("old description"));
Assert.That(result.IconId, Is.EqualTo(_testUser.IconId));
_mockUsersRepo.Verify(r => r.FindByIdAsync(_testUser.Id), Times.Once);
}
[Test]
public void GetUserProfileAsync_Throws_WhenUserNotFound()
{
// Arrange
_mockUsersRepo.Setup(r => r.FindByIdAsync(It.IsAny<Guid>()))
.ReturnsAsync((User)null!);
// Act + Assert
Assert.ThrowsAsync<NullReferenceException>(() =>
_service.GetUserProfileAsync(Guid.NewGuid()));
}
[Test]
public void GetUserProfileAsync_ThrowsNotFoundException_WhenNotFoundByKeyException()
{
// Arrange
var userId = Guid.NewGuid();
_mockUsersRepo.Setup(r => r.FindByIdAsync(It.IsAny<Guid>()))
.ThrowsAsync(new NotFoundByKeyException<Guid>(userId));
// Act + Assert
Assert.ThrowsAsync<NotFoundException>(() =>
_service.GetUserProfileAsync(userId));
}
// ---------- SetDescription ----------
[Test]
public async Task SetDescription_UpdatesUserDescription()
{
// Arrange
var newDescription = "new cool description";
_mockUsersRepo.Setup(r => r.FindByIdAsync(_testUser.Id))
.ReturnsAsync(_testUser);
_mockUsersRepo.Setup(r => r.UpdateAsync(It.IsAny<User>()))
.Returns(Task.CompletedTask);
// Act
await _service.SetDescription(newDescription, _testUser.Id);
// Assert
Assert.That(_testUser.Description, Is.EqualTo(newDescription));
_mockUsersRepo.Verify(r => r.UpdateAsync(It.Is<User>(u => u.Description == newDescription)), Times.Once);
}
[Test]
public void SetDescription_Throws_WhenUserNotFound()
{
// Arrange
_mockUsersRepo.Setup(r => r.FindByIdAsync(It.IsAny<Guid>()))
.ReturnsAsync((User)null!);
// Act + Assert
Assert.ThrowsAsync<NullReferenceException>(() =>
_service.SetDescription("desc", Guid.NewGuid()));
}
// ---------- SetNewIcon ----------
[Test]
public async Task SetNewIcon_UpdatesUserIconId()
{
// Arrange
var newIconId = Guid.NewGuid();
_mockUsersRepo.Setup(r => r.FindByIdAsync(_testUser.Id))
.ReturnsAsync(_testUser);
_mockUsersRepo.Setup(r => r.UpdateAsync(It.IsAny<User>()))
.Returns(Task.CompletedTask);
// Act
await _service.SetNewIcon(_testUser.Id, newIconId);
// Assert
Assert.That(_testUser.IconId, Is.EqualTo(newIconId));
_mockUsersRepo.Verify(r => r.UpdateAsync(It.Is<User>(u => u.IconId == newIconId)), Times.Once);
}
[Test]
public void SetNewIcon_Throws_WhenUserNotFound()
{
// Arrange
_mockUsersRepo.Setup(r => r.FindByIdAsync(It.IsAny<Guid>()))
.ReturnsAsync((User)null!);
// Act + Assert
Assert.ThrowsAsync<NullReferenceException>(() =>
_service.SetNewIcon(Guid.NewGuid(), Guid.NewGuid()));
}
}
@@ -0,0 +1,70 @@
using AutoFixture;
using Govor.Application.Interfaces;
using Govor.Application.Services;
using Govor.Core.Models;
using Govor.Core.Repositories.Groups;
using Govor.Data.Repositories.Exceptions;
using Moq;
namespace Govor.Application.Tests.Services;
[TestFixture]
public class UserGroupsServiceTests
{
private Fixture _fixture;
private Mock<IGroupsRepository> _repositoryMock;
private IUserGroupsService _service;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors
.OfType<ThrowingRecursionBehavior>()
.ToList()
.ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_repositoryMock = new Mock<IGroupsRepository>();
_service = new UserGroupsService(_repositoryMock.Object);
}
[Test]
public async Task GetUserGroups_ShouldReturnAllUserGroups()
{
// Arrange
var chats = _fixture.CreateMany<ChatGroup>();
var userId = chats.First().Members.First().Id;
_repositoryMock.Setup(r => r.GetByUserIdAsync(userId))
.ReturnsAsync([chats.First()]);
// Act
var result = await _service.GetUserGroupsAsync(userId);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Count(), Is.EqualTo(1));
Assert.That(result, Is.EquivalentTo([chats.First()]));
}
[Test]
public async Task GetUserGroups_ButGroupDoesNotExist_ShouldReturnEmptyList()
{
// Arrange
var userId = _fixture.Create<Guid>();
_repositoryMock.Setup(r => r.GetByUserIdAsync(userId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(userId));
// Act
var result = await _service.GetUserGroupsAsync(userId);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Count(), Is.EqualTo(0));
}
}
@@ -0,0 +1,82 @@
using Govor.Application.Interfaces.UserOnlineStatus;
using Govor.Application.Services.UserOnlineStatus;
namespace Govor.Application.Tests.Services.UserOnlineStatus;
[TestFixture]
public class OnlineUserStoreTests
{
private IOnlineUserStore _store;
private Guid _userId1;
private Guid _userId2;
[SetUp]
public void Setup()
{
_store = new OnlineUserStore();
_userId1 = Guid.NewGuid();
_userId2 = Guid.NewGuid();
}
[Test]
public void SetOnlineUser_UserIsMarkedOnline()
{
// Act
_store.SetOnlineUser(_userId1);
// Assert
Assert.That(_store.IsOnline(_userId1), Is.True);
}
[Test]
public void SetOfflineUser_UserIsNoLongerOnline()
{
// Act
_store.SetOnlineUser(_userId1);
_store.SetOfflineUser(_userId1);
// Assert
Assert.That(_store.IsOnline(_userId1), Is.False);
}
[Test]
public void IsOnline_ReturnsFalse_ForUnknownUser()
{
// Act & Assert
Assert.That(_store.IsOnline(Guid.NewGuid()), Is.False);
}
[Test]
public void GetAllOnlineUsers_ReturnsAllCurrentlyOnlineUsers()
{
// Arrange
_store.SetOnlineUser(_userId1);
_store.SetOnlineUser(_userId2);
// Act
var onlineUsers = _store.GetAllOnlineUsers();
// Assert
Assert.That(onlineUsers, Is.EquivalentTo(new[] { _userId1, _userId2 }));
}
[Test]
public void SetOnlineUser_Twice_DoesNotThrow()
{
// Act & Assert
Assert.DoesNotThrow(() =>
{
_store.SetOnlineUser(_userId1);
_store.SetOnlineUser(_userId1);
});
Assert.That(_store.IsOnline(_userId1), Is.True);
}
[Test]
public void SetOfflineUser_ForUnknownUser_DoesNotThrow()
{
// Act & Assert
Assert.DoesNotThrow(() => _store.SetOfflineUser(Guid.NewGuid()));
}
}
@@ -0,0 +1,68 @@
using AutoFixture;
using Govor.Application.Interfaces.Friends;
using Govor.Application.Interfaces.UserOnlineStatus;
using Govor.Application.Services.UserOnlineStatus;
using Govor.Core.Models;
using Govor.Core.Models.Users;
using Govor.Data.Repositories.Exceptions;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.Application.Tests.Services.UserOnlineStatus;
[TestFixture]
[TestOf(typeof(UserNotificationScopeService))]
public class UserNotificationScopeServiceTests
{
private Mock<ILogger<UserNotificationScopeService>> _mockLogger;
private Mock<IFriendshipService> _mockFriendshipService;
private IUserNotificationScopeService _service;
private Fixture _fixture;
private Guid _userId;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_userId = Guid.NewGuid();
_mockLogger = new Mock<ILogger<UserNotificationScopeService>>();
_mockFriendshipService = new Mock<IFriendshipService>();
_service = new UserNotificationScopeService(_mockLogger.Object, _mockFriendshipService.Object);
}
[Test]
public async Task GetNotifiedUsers_ReturnsNotifiedUsers()
{
// Arrange
var random = new Random();
var users = _fixture.CreateMany<User>(random.Next(3, 10)).ToList();
_mockFriendshipService.Setup(f => f.GetFriendsAsync(_userId))
.ReturnsAsync(users);
// Act
var result = await _service.GetNotifiedUsers(_userId);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Count, Is.EqualTo(users.Count));
Assert.That(result, Is.EquivalentTo(users.Select(u => u.Id)));
}
[Test]
public async Task GetNotifiedUsers_ThrowsInvalidOperationException_WhenUserDoesNotExist()
{
// Arrange
_mockFriendshipService.Setup(f => f.GetFriendsAsync(_userId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(_userId, "Database is empty."));
// Act & Assert
var ex = Assert.ThrowsAsync<InvalidOperationException>(async () => await _service.GetNotifiedUsers(_userId));
Assert.That(ex.Message, Is.EqualTo("User not found"));
}
}
@@ -0,0 +1,155 @@
using Govor.Application.Services.UserSessions;
using Govor.Core.Models.Users;
using Govor.Core.Repositories.UserSessionsRepository;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using Govor.Application.Interfaces.Authentication;
using Govor.Application.Services.Authentication;
using Govor.Data.Repositories.Exceptions;
namespace Govor.Application.Tests.Services.UserSessions;
[TestFixture]
public class UserSessionOpenerTests
{
private Mock<IUserSessionsRepository> _repositoryMock;
private Mock<IJwtService> _jwtServiceMock;
private Mock<IJwtTokenHasher> _jwtTokenHasherMock;
private UserSessionOpener _service;
private User _user;
private const string DeviceInfo = "Chrome on Windows";
private const string RefreshToken = "new-refresh-token";
private const string AccessToken = "new-access-token";
private const string TokenHash = "hashed-refresh-token";
[SetUp]
public void Setup()
{
_repositoryMock = new Mock<IUserSessionsRepository>();
_jwtServiceMock = new Mock<IJwtService>();
var loggerMock = new Mock<ILogger<UserSessionOpener>>();
_jwtTokenHasherMock = new Mock<IJwtTokenHasher>();
var options = Options.Create(new JwtRefreshOption { RefreshTokenLifetimeDays = 30 });
_user = new User
{
Id = Guid.NewGuid(),
Username = "test",
Description = "some description",
PasswordHash = "hashed-password",
IconId = Guid.NewGuid(),
CreatedOn = DateOnly.FromDateTime(DateTime.Now),
WasOnline = DateTime.Now,
InviteId = Guid.NewGuid(),
// ...
};
_jwtServiceMock.Setup(j => j.GenerateRefreshTokenAsync(_user)).ReturnsAsync(RefreshToken);
_jwtTokenHasherMock.Setup(h => h.HashToken(RefreshToken)).Returns(TokenHash);
_jwtServiceMock.Setup(j => j.GenerateAccessTokenAsync(_user, It.IsAny<Guid>())).ReturnsAsync(AccessToken);
_service = new UserSessionOpener(
_repositoryMock.Object,
_jwtServiceMock.Object,
_jwtTokenHasherMock.Object,
options,
loggerMock.Object
);
}
// UpdateExistingSessionAsync
[Test]
public async Task OpenSessionAsync_ShouldUpdateExistingSession_WhenFoundByDeviceInfo()
{
// Arrange
var existingSessionId = Guid.NewGuid();
var existingSession = new UserSession
{
Id = existingSessionId,
UserId = _user.Id,
DeviceInfo = DeviceInfo,
RefreshTokenHash = "old-hash",
CreatedAt = DateTime.UtcNow.AddDays(-10),
ExpiresAt = DateTime.UtcNow.AddDays(10),
IsRevoked = true
};
_repositoryMock.Setup(r => r.GetByUserIdAsync(_user.Id)).ReturnsAsync(new List<UserSession> { existingSession });
// Act
var result = await _service.OpenSessionAsync(_user, DeviceInfo);
// Assert
Assert.Multiple(() =>
{
Assert.That(result.refreshToken, Is.EqualTo(RefreshToken), "Должен быть возвращен новый RefreshToken.");
Assert.That(result.accessToken, Is.EqualTo(AccessToken), "Должен быть возвращен новый AccessToken.");
});
_repositoryMock.Verify(r => r.UpdateAsync(It.Is<UserSession>(s =>
s.Id == existingSessionId &&
s.RefreshTokenHash == TokenHash &&
s.IsRevoked == false &&
s.ExpiresAt > DateTime.UtcNow
)), Times.Once, "Должен быть вызван метод UpdateAsync с новыми данными.");
_repositoryMock.Verify(r => r.AddAsync(It.IsAny<UserSession>()), Times.Never);
}
// CreateNewSessionAsync
[Test]
public async Task OpenSessionAsync_ShouldCreateNewSession_IfNoneFound()
{
// Arrange
_repositoryMock.Setup(r => r.GetByUserIdAsync(_user.Id)).ReturnsAsync(new List<UserSession>());
// Act
var result = await _service.OpenSessionAsync(_user, DeviceInfo);
// Assert
Assert.Multiple(() =>
{
Assert.That(result.refreshToken, Is.EqualTo(RefreshToken), "Должен быть возвращен новый RefreshToken.");
Assert.That(result.accessToken, Is.EqualTo(AccessToken), "Должен быть возвращен новый AccessToken.");
});
_repositoryMock.Verify(r => r.AddAsync(It.Is<UserSession>(s =>
s.UserId == _user.Id &&
s.DeviceInfo == DeviceInfo &&
s.RefreshTokenHash == TokenHash &&
s.IsRevoked == false
)), Times.Once, "Должен быть вызван метод AddAsync для создания новой сессии.");
_repositoryMock.Verify(r => r.UpdateAsync(It.IsAny<UserSession>()), Times.Never);
}
[Test]
public async Task OpenSessionAsync_ShouldCreateNewSession_WhenRepositoryThrowsNotFoundByKeyException()
{
// Arrange
_repositoryMock
.Setup(r => r.GetByUserIdAsync(_user.Id))
.ThrowsAsync(new NotFoundByKeyException<Guid>(_user.Id, "userId"));
// Act
var result = await _service.OpenSessionAsync(_user, DeviceInfo);
// Assert
Assert.Multiple(() =>
{
Assert.That(result.refreshToken, Is.EqualTo(RefreshToken), "Должен быть возвращен новый RefreshToken.");
Assert.That(result.accessToken, Is.EqualTo(AccessToken), "Должен быть возвращен новый AccessToken.");
});
_repositoryMock.Verify(r => r.AddAsync(It.Is<UserSession>(s =>
s.RefreshTokenHash == TokenHash
)), Times.Once, "Должен быть вызван AddAsync после перехвата исключения.");
_repositoryMock.Verify(r => r.UpdateAsync(It.IsAny<UserSession>()), Times.Never);
}
}
@@ -0,0 +1,75 @@
using AutoFixture;
using Govor.Application.Interfaces.UserSession;
using Govor.Application.Services.UserSessions;
using Govor.Core.Models;
using Govor.Core.Models.Users;
using Govor.Core.Repositories.UserSessionsRepository;
using Govor.Data.Repositories.Exceptions;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.Application.Tests.Services.UserSessions;
[TestFixture]
[TestOf(typeof(UserSessionReader))]
public class UserSessionReaderTests
{
private Mock<IUserSessionsRepository> _mockUserSessionsRepository;
private Mock<ILogger<UserSessionReader>> _mockLogger;
private Fixture _fixture;
private IUserSessionReader _userSessionReader;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>()
.ToList()
.ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_mockUserSessionsRepository = new Mock<IUserSessionsRepository>();
_mockLogger = new Mock<ILogger<UserSessionReader>>();
_userSessionReader = new UserSessionReader(_mockUserSessionsRepository.Object,
_mockLogger.Object);
}
[Test]
public async Task GetAllUserSessionsAsync_ShouldReturnAllUserSessions()
{
// Arrange
var sessios = _fixture.Build<UserSession>()
.With(f => f.IsRevoked, false)
.CreateMany().ToList();
var userId = Guid.NewGuid();
_mockUserSessionsRepository.Setup(f => f.GetByUserIdAsync(userId))
.ReturnsAsync(sessios);
// Act
var result = await _userSessionReader.GetAllSessionsAsync(userId);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.EquivalentTo(sessios));
}
[Test]
public async Task GetAllUserSessionsAsync_ThrowsNotFoundByKeyException_ReturnsEmptyList()
{
// Arrange
var userId = Guid.NewGuid();
_mockUserSessionsRepository.Setup(f => f.GetByUserIdAsync(userId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(userId));
// Act
var result = await _userSessionReader.GetAllSessionsAsync(userId);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.Empty);
}
}
@@ -0,0 +1,135 @@
using Govor.Application.Interfaces.Authentication;
using Govor.Application.Services.Authentication;
using Govor.Application.Services.UserSessions;
using Govor.Core.Models;
using Govor.Core.Models.Users;
using Govor.Core.Repositories.Users;
using Govor.Core.Repositories.UserSessionsRepository;
using Govor.Data.Repositories.Exceptions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
namespace Govor.Application.Tests.Services.UserSessions;
[TestFixture]
public class UserSessionRefresherTests
{
private Mock<IUserSessionsRepository> _sessionsRepoMock;
private Mock<IUsersRepository> _usersRepoMock;
private Mock<IJwtService> _jwtServiceMock;
private Mock<ILogger<UserSessionRefresher>> _loggerMock;
private Mock<IOptions<JwtRefreshOption>> _optionsMock;
private JwtRefreshOption _options;
private UserSessionRefresher _refresher;
private const string OldRefreshToken = "old-refresh-token";
private const string NewRefreshToken = "new-refresh-token";
private const string NewAccessToken = "new-access-token";
private User _user;
private UserSession _session;
[SetUp]
public void Setup()
{
_sessionsRepoMock = new Mock<IUserSessionsRepository>();
_usersRepoMock = new Mock<IUsersRepository>();
_jwtServiceMock = new Mock<IJwtService>();
_loggerMock = new Mock<ILogger<UserSessionRefresher>>();
_optionsMock = new Mock<IOptions<JwtRefreshOption>>();
_options = new JwtRefreshOption { RefreshTokenLifetimeDays = 30 };
_optionsMock.SetupGet(o => o.Value).Returns(_options);
_refresher = new UserSessionRefresher(
_sessionsRepoMock.Object,
_loggerMock.Object,
_usersRepoMock.Object,
_optionsMock.Object,
_jwtServiceMock.Object);
_user = new User
{
Id = Guid.NewGuid(),
Username = "TestUser",
PasswordHash = "hash",
InviteId = Guid.NewGuid()
};
_session = new UserSession
{
RefreshTokenHash = OldRefreshToken,
UserId = _user.Id,
DeviceInfo = "Chrome",
CreatedAt = DateTime.UtcNow.AddDays(-5),
ExpiresAt = DateTime.UtcNow.AddDays(5),
IsRevoked = false
};
}
[Test]
public async Task RefreshTokenAsync_ValidToken_ReturnsNewTokensAndCreatesNewSession()
{
// Arrange
_sessionsRepoMock.Setup(r => r.GetByHashedRefreshTokenAsync(OldRefreshToken)).ReturnsAsync(_session);
_usersRepoMock.Setup(r => r.FindByIdAsync(_user.Id)).ReturnsAsync(_user);
_jwtServiceMock.Setup(j => j.GenerateAccessTokenAsync(_user, _session.Id)).ReturnsAsync(NewAccessToken);
_jwtServiceMock.Setup(j => j.GenerateRefreshTokenAsync(_user)).ReturnsAsync(NewRefreshToken);
// Act
var result = await _refresher.RefreshTokenAsync(OldRefreshToken);
// Assert
Assert.That(result.accessToken, Is.EqualTo(NewAccessToken));
Assert.That(result.refreshToken, Is.EqualTo(NewRefreshToken));
Assert.That(_session.IsRevoked, Is.True);
_sessionsRepoMock.Verify(r => r.UpdateAsync(_session), Times.Once);
_sessionsRepoMock.Verify(r => r.AddAsync(It.Is<UserSession>(s =>
s.UserId == _user.Id &&
s.RefreshTokenHash == NewRefreshToken &&
s.DeviceInfo == _session.DeviceInfo)), Times.Once);
}
[Test]
public void RefreshTokenAsync_RevokedToken_ThrowsUnauthorizedAccessException()
{
// Arrange
_session.IsRevoked = true;
_sessionsRepoMock.Setup(r => r.GetByHashedRefreshTokenAsync(OldRefreshToken)).ReturnsAsync(_session);
// Act & Assert
var ex = Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
await _refresher.RefreshTokenAsync(OldRefreshToken));
Assert.That(ex.Message, Contains.Substring("Refresh token is invalid or expired"));
}
[Test]
public void RefreshTokenAsync_ExpiredToken_ThrowsUnauthorizedAccessException()
{
// Arrange
_session.ExpiresAt = DateTime.UtcNow.AddMinutes(-1);
_sessionsRepoMock.Setup(r => r.GetByHashedRefreshTokenAsync(OldRefreshToken)).ReturnsAsync(_session);
// Act & Assert
var ex = Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
await _refresher.RefreshTokenAsync(OldRefreshToken));
Assert.That(ex.Message, Contains.Substring("Refresh token is invalid or expired"));
}
[Test]
public void RefreshTokenAsync_TokenNotFound_ThrowsUnauthorizedAccessException()
{
// Arrange
_sessionsRepoMock.Setup(r => r.GetByHashedRefreshTokenAsync(OldRefreshToken))
.ThrowsAsync(new NotFoundByKeyException<string>("token", OldRefreshToken));
// Act & Assert
var ex = Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
await _refresher.RefreshTokenAsync(OldRefreshToken));
Assert.That(ex.Message, Contains.Substring("Invalid refresh token"));
}
}
@@ -0,0 +1,153 @@
using Govor.Application.Services.UserSessions;
using Govor.Core.Models;
using Govor.Core.Models.Users;
using Govor.Core.Repositories.UserSessionsRepository;
using Govor.Data.Repositories.Exceptions;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.Application.Tests.Services.UserSessions;
[TestFixture]
[TestOf(typeof(UserSessionRevoker))]
public class UserSessionRevokerTests
{
private Mock<IUserSessionsRepository> _sessionsRepositoryMock;
private Mock<ILogger<UserSessionRevoker>> _loggerMock;
private UserSessionRevoker _revoker;
[SetUp]
public void SetUp()
{
_sessionsRepositoryMock = new Mock<IUserSessionsRepository>();
_loggerMock = new Mock<ILogger<UserSessionRevoker>>();
_revoker = new UserSessionRevoker(_sessionsRepositoryMock.Object, _loggerMock.Object);
}
[Test]
public async Task CloseSessionByIdAsync_ValidSessionAndUserId_SetsIsRevokedAndUpdates()
{
// Arrange
var sessionId = Guid.NewGuid();
var userId = Guid.NewGuid();
var session = new UserSession { Id = sessionId, UserId = userId, IsRevoked = false };
_sessionsRepositoryMock.Setup(r => r.GetByIdAsync(sessionId)).ReturnsAsync(session);
// Act
await _revoker.CloseSessionByIdAsync(sessionId, userId);
// Assert
Assert.That(session.IsRevoked, Is.True);
_sessionsRepositoryMock.Verify(r => r.UpdateAsync(session), Times.Once());
_loggerMock.VerifyNoOtherCalls();
}
[Test]
public void CloseSessionByIdAsync_UnauthorizedUser_ThrowsUnauthorizedAccessException()
{
// Arrange
var sessionId = Guid.NewGuid();
var userId = Guid.NewGuid();
var differentUserId = Guid.NewGuid();
var session = new UserSession { Id = sessionId, UserId = userId, IsRevoked = false };
_sessionsRepositoryMock.Setup(r => r.GetByIdAsync(sessionId)).ReturnsAsync(session);
// Act & Assert
var ex = Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
_revoker.CloseSessionByIdAsync(sessionId, differentUserId));
Assert.That(ex.Message, Contains.Substring($"User {differentUserId} does not belong to this session {sessionId}"));
_loggerMock.Verify(l => l.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once());
_sessionsRepositoryMock.Verify(r => r.UpdateAsync(It.IsAny<UserSession>()), Times.Never());
}
[Test]
public async Task CloseSessionByIdAsync_AlreadyRevokedSession_DoesNotUpdate()
{
// Arrange
var sessionId = Guid.NewGuid();
var userId = Guid.NewGuid();
var session = new UserSession { Id = sessionId, UserId = userId, IsRevoked = true };
_sessionsRepositoryMock.Setup(r => r.GetByIdAsync(sessionId)).ReturnsAsync(session);
// Act
await _revoker.CloseSessionByIdAsync(sessionId, userId);
// Assert
_sessionsRepositoryMock.Verify(r => r.UpdateAsync(It.IsAny<UserSession>()), Times.Never());
_loggerMock.VerifyNoOtherCalls();
}
[Test]
public void CloseSessionByIdAsync_NonExistentSession_ThrowsNotFoundException()
{
// Arrange
var sessionId = Guid.NewGuid();
var userId = Guid.NewGuid();
_sessionsRepositoryMock.Setup(r => r.GetByIdAsync(sessionId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(sessionId));
// Act & Assert
var ex = Assert.ThrowsAsync<NotFoundException>(() =>
_revoker.CloseSessionByIdAsync(sessionId, userId));
Assert.That(ex.Message, Is.EqualTo("Session not found"));
_loggerMock.Verify(l => l.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once());
_sessionsRepositoryMock.Verify(r => r.UpdateAsync(It.IsAny<UserSession>()), Times.Never());
}
[Test]
public async Task CloseAllSessionsAsync_MultipleSessions_RevokesNonRevokedSessions()
{
// Arrange
var userId = Guid.NewGuid();
var sessions = new List<UserSession>
{
new UserSession { Id = Guid.NewGuid(), UserId = userId, IsRevoked = false },
new UserSession { Id = Guid.NewGuid(), UserId = userId, IsRevoked = true },
new UserSession { Id = Guid.NewGuid(), UserId = userId, IsRevoked = false }
};
_sessionsRepositoryMock.Setup(r => r.GetByUserIdAsync(userId)).ReturnsAsync(sessions);
// Act
await _revoker.CloseAllSessionsAsync(userId);
// Assert
Assert.That(sessions[0].IsRevoked, Is.True);
Assert.That(sessions[1].IsRevoked, Is.True);
Assert.That(sessions[2].IsRevoked, Is.True);
_sessionsRepositoryMock.Verify(r => r.UpdateAsync(sessions[0]), Times.Once());
_sessionsRepositoryMock.Verify(r => r.UpdateAsync(sessions[2]), Times.Once());
_sessionsRepositoryMock.Verify(r => r.UpdateAsync(sessions[1]), Times.Never());
_loggerMock.VerifyNoOtherCalls();
}
[Test]
public void CloseAllSessionsAsync_NoSessions_ThrowsNotFoundException()
{
// Arrange
var userId = Guid.NewGuid();
_sessionsRepositoryMock.Setup(r => r.GetByUserIdAsync(userId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(userId));
// Act & Assert
var ex = Assert.ThrowsAsync<NotFoundException>(() =>
_revoker.CloseAllSessionsAsync(userId));
Assert.That(ex.Message, Is.EqualTo("Session not found"));
_loggerMock.Verify(l => l.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once());
_sessionsRepositoryMock.Verify(r => r.UpdateAsync(It.IsAny<UserSession>()), Times.Never());
}
}
@@ -0,0 +1,96 @@
using AutoFixture;
using Govor.Application.Exceptions.VerifyFriendship;
using Govor.Application.Services;
using Govor.Core.Models;
using Govor.Core.Repositories.Friendships;
using Govor.Data.Repositories.Exceptions;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.Application.Tests.Services;
[TestFixture]
public class VerifyFriendshipTests
{
private Fixture _fixture;
private Mock<ILogger<VerifyFriendship>> _mockLogger;
private Mock<IFriendshipsRepository> _mockFriendships;
private VerifyFriendship _service;
[SetUp]
public void SetUp()
{
_fixture = new Fixture();
_fixture.Behaviors
.OfType<ThrowingRecursionBehavior>()
.ToList()
.ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_mockLogger = new Mock<ILogger<VerifyFriendship>>();
_mockFriendships = new Mock<IFriendshipsRepository>();
_service = new VerifyFriendship(_mockFriendships.Object, _mockLogger.Object);
}
// Test for VerifyAsync action
[Test]
public async Task VerifyAsync_Success()
{
// Arrange
var friendship1 = _fixture.Create<Friendship>();
friendship1.Status = FriendshipStatus.Accepted;
_mockFriendships.Setup(f => f.FindByUserIdAsync(friendship1.RequesterId))
.ReturnsAsync([friendship1]);
// Act
await _service.VerifyAsync(friendship1.RequesterId, friendship1.AddresseeId);
// Assert
_mockFriendships.Verify(f => f.FindByUserIdAsync(friendship1.RequesterId), Times.Once());
Assert.That(await _service.TryVerifyAsync(friendship1.RequesterId, friendship1.AddresseeId), Is.EqualTo(true));
}
[Test]
public async Task VerifyAsync_IDsEmpty()
{
// Arrange
var friendship1 = _fixture.Create<Friendship>();
friendship1.RequesterId = Guid.Empty;
friendship1.AddresseeId = Guid.Empty;
// Act & Assert
Assert.ThrowsAsync<ArgumentException>(() => _service.VerifyAsync(friendship1.RequesterId, friendship1.AddresseeId));
Assert.That(await _service.TryVerifyAsync(friendship1.RequesterId, friendship1.AddresseeId), Is.EqualTo(false));
}
[Test]
public async Task VerifyAsync_ThrowsNotFoundByKeyException_ThrowsFriendshipException()
{
// Arrange
var friendship1 = _fixture.Create<Friendship>();
friendship1.Status = FriendshipStatus.Accepted;
_mockFriendships.Setup(f => f.FindByUserIdAsync(friendship1.RequesterId))
.ThrowsAsync(new NotFoundByKeyException<Guid>(friendship1.RequesterId));
// Act & Assert
Assert.ThrowsAsync<FriendshipException>(async () => await _service.VerifyAsync(friendship1.RequesterId, friendship1.AddresseeId));
_mockFriendships.Verify(f => f.FindByUserIdAsync(friendship1.RequesterId), Times.Once());
Assert.That(await _service.TryVerifyAsync(friendship1.RequesterId, friendship1.AddresseeId), Is.EqualTo(false));
}
[Test]
public async Task VerifyAsync_ReturnsEmptyFriendships_ThrowsFriendshipException()
{
// Arrange
var friendship1 = _fixture.Create<Friendship>();
friendship1.Status = FriendshipStatus.Accepted;
_mockFriendships.Setup(f => f.FindByUserIdAsync(friendship1.RequesterId))
.ReturnsAsync(new List<Friendship>());
// Act & Assert
Assert.ThrowsAsync<FriendshipException>(async () => await _service.VerifyAsync(friendship1.RequesterId, friendship1.AddresseeId));
_mockFriendships.Verify(f => f.FindByUserIdAsync(friendship1.RequesterId), Times.Once());
Assert.That(await _service.TryVerifyAsync(friendship1.RequesterId, friendship1.AddresseeId), Is.EqualTo(false));
}
}
@@ -0,0 +1,9 @@
using Govor.Core;
namespace Govor.Application.Exceptions.VerifyFriendship;
public class FriendshipException : GovorCoreException
{
public FriendshipException(string s)
:base(s) { }
}
+2 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
@@ -14,6 +14,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.3.0" /> <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.0.1" /> <PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
</ItemGroup> </ItemGroup>
@@ -1,8 +1,8 @@
using Govor.API.Services.AdminsStuff.Interfaces; using Govor.Application.Interfaces;
using Govor.Core.Models; using Govor.Core.Models;
using Govor.Core.Repositories.Invaites; using Govor.Core.Repositories.Invaites;
namespace Govor.Application.Interfaces.AdminsStuff; namespace Govor.Application.Infrastructure.AdminsStuff;
public class InvitationGenerator(IInvitesRepository repository) : IInvitationGenerator public class InvitationGenerator(IInvitesRepository repository) : IInvitationGenerator
{ {
@@ -1,9 +1,9 @@
using Govor.API.Services.AdminsStuff.Interfaces; using Govor.Application.Interfaces;
using Govor.Core.Models; using Govor.Core.Models.Users;
using Govor.Core.Repositories.Users; using Govor.Core.Repositories.Users;
using Govor.Data.Repositories.Exceptions; using Govor.Data.Repositories.Exceptions;
namespace Govor.Application.Interfaces.AdminsStuff; namespace Govor.Application.Infrastructure.AdminsStuff;
public class UsersService : IUsersAdministration public class UsersService : IUsersAdministration
{ {
@@ -26,4 +26,11 @@ public class UsersService : IUsersAdministration
return new List<User>(); return new List<User>();
} }
} }
public async Task<User> GetUserById(Guid userId)
{
var result = await _usersRepository.FindByIdAsync(userId);
return result;
}
} }
@@ -6,21 +6,23 @@ namespace Govor.Application.Infrastructure.Extensions;
public class CurrentUserService : ICurrentUserService public class CurrentUserService : ICurrentUserService
{ {
private readonly ClaimsPrincipal _user; private readonly IHttpContextAccessor _httpContextAccessor;
public CurrentUserService(IHttpContextAccessor httpContextAccessor) public CurrentUserService(IHttpContextAccessor httpContextAccessor)
{ {
_user = httpContextAccessor.HttpContext.User; _httpContextAccessor = httpContextAccessor;
} }
public Guid GetCurrentUserId() public Guid GetCurrentUserId()
{ {
var userIdClaim = _user.FindFirst("userId")?.Value; var user = _httpContextAccessor.HttpContext?.User;
var userIdClaim = user?.FindFirst("userId")?.Value;
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId)) if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
{ {
throw new UnauthorizedAccessException("userID claim is missing or invalid"); throw new UnauthorizedAccessException("userID claim is missing or invalid");
} }
return userId; return userId;
} }
} }
@@ -0,0 +1,27 @@
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Microsoft.AspNetCore.Http;
namespace Govor.Application.Infrastructure.Extensions;
public class CurrentUserSessionService : ICurrentUserSessionService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CurrentUserSessionService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public Guid GetUserSessionId()
{
var user = _httpContextAccessor.HttpContext?.User;
var userIdClaim = user?.FindFirst("sid")?.Value;
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
{
throw new UnauthorizedAccessException("Session id (sid) claim is missing or invalid");
}
return userId;
}
}
@@ -1,14 +1,13 @@
using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Govor.Application.Exceptions.AuthService; using Govor.Application.Exceptions.AuthService;
using Govor.Application.Interfaces.Authentication; using Govor.Application.Interfaces.Authentication;
using Govor.Core.Infrastructure.Validators; using Govor.Core.Infrastructure.Validators;
namespace Govor.Application.Validators; namespace Govor.Application.Infrastructure.Validators;
public class UsernameValidator : IUsernameValidator public class UsernameValidator : IUsernameValidator
{ {
private readonly Regex _usernameRegex = new(@"^[А-Яа-яЁё]+$", RegexOptions.Compiled); private readonly Regex _usernameRegex = new(@"^[А-Яа-яЁё]+[А-Яа-яЁё0-9]*$", RegexOptions.Compiled);
public void Validate(string username) public void Validate(string username)
{ {
@@ -1,9 +1,10 @@
using Govor.Core.Models; using Govor.Core.Models;
using Govor.Core.Models.Users;
namespace Govor.Application.Interfaces.Authentication; namespace Govor.Application.Interfaces.Authentication;
public interface IAccountService public interface IAccountService
{ {
public Task<string> RegistrationAsync(string name, string password, Invitation invitation); public Task<User> RegistrationAsync(string name, string password, Invitation invitation);
public Task<string> LoginAsync(string name, string password); public Task<User> LoginAsync(string name, string password);
} }
@@ -1,9 +1,10 @@
using Govor.Core.Models; using Govor.Core.Models;
using Govor.Core.Models.Users;
namespace Govor.API.Services.Authentication.Interfaces; namespace Govor.Application.Interfaces.Authentication;
public interface IInvitesService public interface IInvitesService
{ {
public Task<string> GetRole(User user); public Task<string> GetRoleAsync(User user);
public Invitation Validate(string inviteCode); public Task<Invitation> ValidateAsync(string inviteCode);
} }
@@ -1,8 +1,11 @@
using Govor.Core.Models; using System.Security.Claims;
using Govor.Core.Models.Users;
namespace Govor.API.Services.Authentication.Interfaces; namespace Govor.Application.Interfaces.Authentication;
public interface IJwtService public interface IJwtService
{ {
string GenerateJwtToken(User user); Task<string> GenerateAccessTokenAsync(User user, Guid sessionId);
Task<string> GenerateRefreshTokenAsync(User user);
ClaimsPrincipal GetPrincipalFromExpiredToken(string token);
} }
@@ -0,0 +1,7 @@
namespace Govor.Application.Interfaces.Authentication;
public interface IJwtTokenHasher
{
string HashToken(string token);
bool VerifyToken(string token, string storedHash);
}
@@ -0,0 +1,10 @@
using Govor.Core.Models;
namespace Govor.Application.Interfaces.Friends;
public interface IFriendRequestCommandService
{
Task<Friendship> SendAsync(Guid fromUserId, Guid toUserId);
Task<Friendship> AcceptAsync(Guid requestId, Guid currentUserId);
Task<Friendship> RejectAsync(Guid requestId, Guid currentUserId);
}
@@ -0,0 +1,10 @@
using Govor.Core.Models;
namespace Govor.Application.Interfaces.Friends;
public interface IFriendRequestQueryService
{
Task<List<Friendship>> GetIncomingAsync(Guid userId);
Task<List<Friendship>> GetResponsesAsync(Guid userId);
}
@@ -0,0 +1,7 @@
namespace Govor.Application.Interfaces;
public interface IFriendsBlockService
{
Task BlockFriendRequestAsync(Guid userId, Guid currentUserId);
Task UnblockFriendRequestAsync(Guid userId, Guid currentUserId);
}
@@ -0,0 +1,9 @@
using Govor.Core.Models.Users;
namespace Govor.Application.Interfaces.Friends;
public interface IFriendshipService
{
Task<List<User>> GetFriendsAsync(Guid userId);
Task<List<User>> SearchUsersAsync(string query, Guid currentId);
}
@@ -1,4 +1,5 @@
using Govor.Core.Models; using Govor.Core.Models;
using Govor.Core.Models.Users;
namespace Govor.Application.Interfaces; namespace Govor.Application.Interfaces;
@@ -7,6 +8,10 @@ public interface IFriendsService
Task<List<User>> SearchUsersAsync(string query, Guid currentId); Task<List<User>> SearchUsersAsync(string query, Guid currentId);
Task SendFriendRequestAsync(Guid fromUserId, Guid toUserId); Task SendFriendRequestAsync(Guid fromUserId, Guid toUserId);
Task AcceptFriendRequestAsync(Guid requestId, Guid currentUserId); Task AcceptFriendRequestAsync(Guid requestId, Guid currentUserId);
Task RejectFriendRequestAsync(Guid requestId, Guid currentUserId);
Task<List<User>> GetFriendsAsync(Guid userId); Task<List<User>> GetFriendsAsync(Guid userId);
Task<List<Friendship>> GetResponsesAsync(Guid userId);
Task<List<Friendship>> GetIncomingRequestsAsync(Guid userId); Task<List<Friendship>> GetIncomingRequestsAsync(Guid userId);
} }

Some files were not shown because too many files have changed in this diff Show More