From d3f1e3f3611cc5a8bec9ae66518034ee12c3c5d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A5=D0=B0=D0=BB=D0=B8=D0=BC=D0=BE=D0=B2=20=D0=A0=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BC?= Date: Mon, 30 Mar 2026 23:41:01 +0300 Subject: [PATCH] =?UTF-8?q?=D0=AD=D0=BD=D0=B4=D0=BF=D0=BE=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...Knot.Modules.Admin.IntegrationTests.csproj | 26 +++ .../Knot.Modules.Admin.UnitTests.csproj | 30 ++++ .../Knot.Modules.Settings.UnitTests.csproj | 28 +++ .../ResetUserPasswordCommandHandlerTests.cs | 58 +++++++ .../Knot.Modules.Auth.IntegrationTests.csproj | 23 +++ .../Knot.Modules.Auth.UnitTests.csproj | 32 ++++ .../UnitTests/LoginUserCommandHandlerTests.cs | 88 ++++++++++ .../Chats/CreateChatCommandHandlerTests.cs | 54 ++++++ .../Chats/GetChatsQueryHandlerTests.cs | 74 ++++++++ ...GetOrCreateFavoritesCommandHandlerTests.cs | 74 ++++++++ ...not.Modules.Conversations.UnitTests.csproj | 33 ++++ .../SendMessageCommandHandlerTests.cs | 107 ++++++++++++ ...Modules.Federation.IntegrationTests.csproj | 23 +++ .../Knot.Modules.Federation.UnitTests.csproj | 30 ++++ .../Knot.Modules.Klipy.UnitTests.csproj | 30 ++++ ....Modules.Messaging.IntegrationTests.csproj | 23 +++ .../Knot.Modules.Messaging.UnitTests.csproj | 24 +++ ....Modules.Relations.IntegrationTests.csproj | 23 +++ .../Knot.Modules.Relations.UnitTests.csproj | 30 ++++ .../BaseIntegrationTest.cs | 88 ++++++++++ .../Knot.IntegrationTests.Shared.csproj | 38 +++++ .../Knot.Modules.Storage.UnitTests.csproj | 30 ++++ ...ot.Modules.Stories.IntegrationTests.csproj | 26 +++ .../Stories/IntegrationTests/StoriesTests.cs | 34 ++++ .../CreateStoryCommandHandlerTests.cs | 76 +++++++++ .../DeleteStoryCommandHandlerTests.cs | 76 +++++++++ .../Knot.Modules.Stories.UnitTests.csproj | 30 ++++ ...ot.Modules.TelegramImport.UnitTests.csproj | 30 ++++ .../Knot.Modules.Profiles.UnitTests.csproj | 30 ++++ .../UpdateProfileCommandHandlerTests.cs | 38 +++++ .../Knot.Modules.WebRtc.UnitTests.csproj | 30 ++++ .../Abstractions/IIdentityDbContext.cs | 14 ++ .../Abstractions/IIdentityUnitOfWork.cs | 11 ++ .../Abstractions/IJwtTokenProvider.cs | 9 + .../Application/Auth/DTOs/AuthResponseDto.cs | 21 +++ .../Application/Users/Auth/AuthResponseDto.cs | 21 +++ backend/src/Modules/Auth/Domain/AuthErrors.cs | 15 ++ .../Modules/Auth/Domain/IUserRepository.cs | 19 +++ .../Mongo/MongoDbMapConfigurator.cs | 4 +- .../Profiles/Domain/IAvatarStorageService.cs | 11 ++ .../Profiles/Domain/IProfileRepository.cs | 15 ++ .../Profiles/Domain/IProfilesUnitOfWork.cs | 9 + .../Modules/Profiles/Domain/ProfilesErrors.cs | 10 ++ .../Settings/Abstractions/ISettingsService.cs | 52 ++++++ .../Settings/DTOs/PublicConfigDto.cs | 159 ++++++++++++++++++ .../Settings/DTOs/SystemSettingsDto.cs | 112 ++++++++++++ .../Application/Abstractions/IKlipyClient.cs | 11 ++ .../Abstractions/IStoriesDbContext.cs | 21 +++ .../Infrastructure/External/KlipyClient.cs | 40 +++++ .../stories/infrastructure/storyApi.ts | 5 +- 50 files changed, 1890 insertions(+), 5 deletions(-) create mode 100644 backend/Tests/Admin/IntegrationTests/Knot.Modules.Admin.IntegrationTests.csproj create mode 100644 backend/Tests/Admin/UnitTests/Knot.Modules.Admin.UnitTests.csproj create mode 100644 backend/Tests/Admin/UnitTests/Knot.Modules.Settings.UnitTests.csproj create mode 100644 backend/Tests/Admin/UnitTests/ResetUserPasswordCommandHandlerTests.cs create mode 100644 backend/Tests/Auth/IntegrationTests/Knot.Modules.Auth.IntegrationTests.csproj create mode 100644 backend/Tests/Auth/UnitTests/Knot.Modules.Auth.UnitTests.csproj create mode 100644 backend/Tests/Auth/UnitTests/LoginUserCommandHandlerTests.cs create mode 100644 backend/Tests/Conversations/UnitTests/Chats/CreateChatCommandHandlerTests.cs create mode 100644 backend/Tests/Conversations/UnitTests/Chats/GetChatsQueryHandlerTests.cs create mode 100644 backend/Tests/Conversations/UnitTests/GetOrCreateFavoritesCommandHandlerTests.cs create mode 100644 backend/Tests/Conversations/UnitTests/Knot.Modules.Conversations.UnitTests.csproj create mode 100644 backend/Tests/Conversations/UnitTests/Messages/SendMessageCommandHandlerTests.cs create mode 100644 backend/Tests/Federation/IntegrationTests/Knot.Modules.Federation.IntegrationTests.csproj create mode 100644 backend/Tests/Federation/UnitTests/Knot.Modules.Federation.UnitTests.csproj create mode 100644 backend/Tests/Klipy/UnitTests/Knot.Modules.Klipy.UnitTests.csproj create mode 100644 backend/Tests/Messaging/IntegrationTests/Knot.Modules.Messaging.IntegrationTests.csproj create mode 100644 backend/Tests/Messaging/UnitTests/Knot.Modules.Messaging.UnitTests.csproj create mode 100644 backend/Tests/Relations/IntegrationTests/Knot.Modules.Relations.IntegrationTests.csproj create mode 100644 backend/Tests/Relations/UnitTests/Knot.Modules.Relations.UnitTests.csproj create mode 100644 backend/Tests/Shared/IntegrationTests.Shared/BaseIntegrationTest.cs create mode 100644 backend/Tests/Shared/IntegrationTests.Shared/Knot.IntegrationTests.Shared.csproj create mode 100644 backend/Tests/Storage/UnitTests/Knot.Modules.Storage.UnitTests.csproj create mode 100644 backend/Tests/Stories/IntegrationTests/Knot.Modules.Stories.IntegrationTests.csproj create mode 100644 backend/Tests/Stories/IntegrationTests/StoriesTests.cs create mode 100644 backend/Tests/Stories/UnitTests/CreateStoryCommandHandlerTests.cs create mode 100644 backend/Tests/Stories/UnitTests/DeleteStoryCommandHandlerTests.cs create mode 100644 backend/Tests/Stories/UnitTests/Knot.Modules.Stories.UnitTests.csproj create mode 100644 backend/Tests/TelegramImport/UnitTests/Knot.Modules.TelegramImport.UnitTests.csproj create mode 100644 backend/Tests/Users/UnitTests/Knot.Modules.Profiles.UnitTests.csproj create mode 100644 backend/Tests/Users/UnitTests/UpdateProfileCommandHandlerTests.cs create mode 100644 backend/Tests/WebRtc/UnitTests/Knot.Modules.WebRtc.UnitTests.csproj create mode 100644 backend/src/Modules/Auth/Application/Abstractions/IIdentityDbContext.cs create mode 100644 backend/src/Modules/Auth/Application/Abstractions/IIdentityUnitOfWork.cs create mode 100644 backend/src/Modules/Auth/Application/Abstractions/IJwtTokenProvider.cs create mode 100644 backend/src/Modules/Auth/Application/Auth/DTOs/AuthResponseDto.cs create mode 100644 backend/src/Modules/Auth/Application/Users/Auth/AuthResponseDto.cs create mode 100644 backend/src/Modules/Auth/Domain/AuthErrors.cs create mode 100644 backend/src/Modules/Auth/Domain/IUserRepository.cs create mode 100644 backend/src/Modules/Profiles/Domain/IAvatarStorageService.cs create mode 100644 backend/src/Modules/Profiles/Domain/IProfileRepository.cs create mode 100644 backend/src/Modules/Profiles/Domain/IProfilesUnitOfWork.cs create mode 100644 backend/src/Modules/Profiles/Domain/ProfilesErrors.cs create mode 100644 backend/src/Modules/Settings/Application/Settings/Abstractions/ISettingsService.cs create mode 100644 backend/src/Modules/Settings/Application/Settings/DTOs/PublicConfigDto.cs create mode 100644 backend/src/Modules/Settings/Application/Settings/DTOs/SystemSettingsDto.cs create mode 100644 backend/src/Modules/Stories/Application/Abstractions/IKlipyClient.cs create mode 100644 backend/src/Modules/Stories/Application/Abstractions/IStoriesDbContext.cs create mode 100644 backend/src/Modules/Stories/Infrastructure/External/KlipyClient.cs diff --git a/backend/Tests/Admin/IntegrationTests/Knot.Modules.Admin.IntegrationTests.csproj b/backend/Tests/Admin/IntegrationTests/Knot.Modules.Admin.IntegrationTests.csproj new file mode 100644 index 0000000..b28cb9d --- /dev/null +++ b/backend/Tests/Admin/IntegrationTests/Knot.Modules.Admin.IntegrationTests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Admin/UnitTests/Knot.Modules.Admin.UnitTests.csproj b/backend/Tests/Admin/UnitTests/Knot.Modules.Admin.UnitTests.csproj new file mode 100644 index 0000000..3d58229 --- /dev/null +++ b/backend/Tests/Admin/UnitTests/Knot.Modules.Admin.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Admin/UnitTests/Knot.Modules.Settings.UnitTests.csproj b/backend/Tests/Admin/UnitTests/Knot.Modules.Settings.UnitTests.csproj new file mode 100644 index 0000000..9b38437 --- /dev/null +++ b/backend/Tests/Admin/UnitTests/Knot.Modules.Settings.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Admin/UnitTests/ResetUserPasswordCommandHandlerTests.cs b/backend/Tests/Admin/UnitTests/ResetUserPasswordCommandHandlerTests.cs new file mode 100644 index 0000000..3c27630 --- /dev/null +++ b/backend/Tests/Admin/UnitTests/ResetUserPasswordCommandHandlerTests.cs @@ -0,0 +1,58 @@ +using FluentAssertions; +using Knot.Modules.Admin.Application.Admin.Commands; +using Knot.Modules.Auth.Application.Abstractions; +using Knot.Modules.Auth.Domain; +using Knot.Shared.Kernel; +using NSubstitute; +using Xunit; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Modules.Admin.UnitTests; + +public class ResetUserPasswordCommandHandlerTests +{ + private readonly IAuthUnitOfWork _authUnitOfWork; + private readonly IUserRepository _userRepository; + private readonly ResetUserPasswordCommandHandler _handler; + + public ResetUserPasswordCommandHandlerTests() + { + _authUnitOfWork = Substitute.For(); + _userRepository = Substitute.For(); + _handler = new ResetUserPasswordCommandHandler(_userRepository, _authUnitOfWork); + } + + [Fact] + public async Task Handle_ShouldReturnError_WhenUserNotFound() + { + // Arrange + var command = new ResetUserPasswordCommand(Guid.NewGuid(), "NewP@ssw0rd"); + _userRepository.GetByIdAsync(command.UserId, Arg.Any()).Returns((User?)null); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Should().Be(AuthErrors.UserNotFound); + } + + [Fact] + public async Task Handle_ShouldSucceed_WhenUserFound() + { + // Arrange + var user = User.Create("User", "pass", "salt", "admin"); + var command = new ResetUserPasswordCommand(user.Id, "NewP@ssw0rd"); + + _userRepository.GetByIdAsync(command.UserId, Arg.Any()).Returns(user); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + await _authUnitOfWork.Received(1).SaveChangesAsync(Arg.Any()); + } +} diff --git a/backend/Tests/Auth/IntegrationTests/Knot.Modules.Auth.IntegrationTests.csproj b/backend/Tests/Auth/IntegrationTests/Knot.Modules.Auth.IntegrationTests.csproj new file mode 100644 index 0000000..1607c89 --- /dev/null +++ b/backend/Tests/Auth/IntegrationTests/Knot.Modules.Auth.IntegrationTests.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Auth/UnitTests/Knot.Modules.Auth.UnitTests.csproj b/backend/Tests/Auth/UnitTests/Knot.Modules.Auth.UnitTests.csproj new file mode 100644 index 0000000..ab17424 --- /dev/null +++ b/backend/Tests/Auth/UnitTests/Knot.Modules.Auth.UnitTests.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + enable + enable + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/backend/Tests/Auth/UnitTests/LoginUserCommandHandlerTests.cs b/backend/Tests/Auth/UnitTests/LoginUserCommandHandlerTests.cs new file mode 100644 index 0000000..518e53d --- /dev/null +++ b/backend/Tests/Auth/UnitTests/LoginUserCommandHandlerTests.cs @@ -0,0 +1,88 @@ +using FluentAssertions; +using NSubstitute; +using Knot.Modules.Auth.Application.Abstractions; +using Knot.Modules.Auth.Application.Users.Login; +using Knot.Modules.Auth.Domain; +using Knot.Shared.Kernel; +using Knot.Modules.Auth.Application.Users.Auth; +using Xunit; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Modules.Auth.UnitTests; + +public class LoginUserCommandHandlerTests +{ + private readonly IUserRepository _userRepository; + private readonly IJwtTokenProvider _tokenProvider; + private readonly LoginUserCommandHandler _handler; + + public LoginUserCommandHandlerTests() + { + _userRepository = Substitute.For(); + _tokenProvider = Substitute.For(); + _handler = new LoginUserCommandHandler(_userRepository, _tokenProvider); + } + + [Fact] + public async Task Handle_ShouldReturnToken_WhenCredentialsAreValid() + { + // Arrange + var password = "password123"; + var passwordHash = BCrypt.Net.BCrypt.HashPassword(password); + var user = User.Create("testuser", passwordHash, "Test User", null, null); + + var command = new LoginUserCommand("testuser", password); + + _userRepository.GetByUsernameAsync(command.Username, Arg.Any()) + .Returns(user); + + _tokenProvider.Generate(user).Returns("valid-jwt-token"); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Token.Should().Be("valid-jwt-token"); + result.Value.User.Username.Should().Be("testuser"); + } + + [Fact] + public async Task Handle_ShouldReturnFailure_WhenUserDoesNotExist() + { + // Arrange + var command = new LoginUserCommand("nonexistent", "password123"); + _userRepository.GetByUsernameAsync(command.Username, Arg.Any()) + .Returns((User)null!); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Code.Should().Be(AuthErrors.IdentityInvalidCredentials.Code); + } + + [Fact] + public async Task Handle_ShouldReturnFailure_WhenPasswordIsInvalid() + { + // Arrange + var correctPassword = "correctPassword"; + var passwordHash = BCrypt.Net.BCrypt.HashPassword(correctPassword); + var user = User.Create("testuser", passwordHash, "Test User", null, null); + + var command = new LoginUserCommand("testuser", "wrongPassword"); + + _userRepository.GetByUsernameAsync(command.Username, Arg.Any()) + .Returns(user); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Code.Should().Be(AuthErrors.IdentityInvalidCredentials.Code); + } +} + diff --git a/backend/Tests/Conversations/UnitTests/Chats/CreateChatCommandHandlerTests.cs b/backend/Tests/Conversations/UnitTests/Chats/CreateChatCommandHandlerTests.cs new file mode 100644 index 0000000..4de4ea6 --- /dev/null +++ b/backend/Tests/Conversations/UnitTests/Chats/CreateChatCommandHandlerTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using NSubstitute; +using Xunit; +using Knot.Shared.Kernel; +using Knot.Modules.Conversations.Domain; +using Knot.Modules.Messaging.Domain; +using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Modules.Conversations.Application.Chats.Create; + +namespace Knot.Modules.Conversations.UnitTests.Chats; + +public class CreateChatCommandHandlerTests +{ + private readonly IChatRepository _chatRepository; + private readonly IChatsUnitOfWork _unitOfWork; + private readonly CreateChatCommandHandler _handler; + + public CreateChatCommandHandlerTests() + { + _chatRepository = Substitute.For(); + _unitOfWork = Substitute.For(); + _handler = new CreateChatCommandHandler(_chatRepository, _unitOfWork); + } + + [Fact] + public async Task Handle_ShouldCreateChatAndAddMembers() + { + // Arrange + var request = new CreateChatCommand("Test Group", ChatType.Group, new List { Guid.NewGuid(), Guid.NewGuid() }); + + // Act + var result = await _handler.Handle(request, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeEmpty(); + + _chatRepository.Received(1).Add(Arg.Is(c => + c.Name == "Test Group" && + c.Type == ChatType.Group && + c.Members.Count == 2 && + c.Members.First().Role == ChatRole.Owner && + c.Members.Last().Role == ChatRole.Member)); + + await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any()); + } +} + + diff --git a/backend/Tests/Conversations/UnitTests/Chats/GetChatsQueryHandlerTests.cs b/backend/Tests/Conversations/UnitTests/Chats/GetChatsQueryHandlerTests.cs new file mode 100644 index 0000000..a009178 --- /dev/null +++ b/backend/Tests/Conversations/UnitTests/Chats/GetChatsQueryHandlerTests.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using NSubstitute; +using Xunit; +using Knot.Shared.Kernel; +using Knot.Modules.Conversations.Domain; +using Knot.Modules.Messaging.Domain; +using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Modules.Conversations.Application.Chats.GetChats; +using Knot.Modules.Conversations.Application.DTOs; + +namespace Knot.Modules.Conversations.UnitTests.Chats; + +public class GetChatsQueryHandlerTests +{ + private readonly IChatRepository _chatRepository; + private readonly IUserDisplayNameProvider _userProvider; + private readonly IMessageRepository _messageRepository; + private readonly IMessageReactionRepository _reactionRepository; + private readonly GetChatsQueryHandler _handler; + + public GetChatsQueryHandlerTests() + { + _chatRepository = Substitute.For(); + _userProvider = Substitute.For(); + _messageRepository = Substitute.For(); + _reactionRepository = Substitute.For(); + + _handler = new GetChatsQueryHandler(_chatRepository, _userProvider, _messageRepository, _reactionRepository); + } + + [Fact] + public async Task Handle_ShouldReturnUserChats_WhenTheyExist() + { + // Arrange + var userId = Guid.NewGuid(); + var request = new GetChatsQuery(userId); + + var chat1 = Chat.Create("Test Chat", ChatType.Group); + chat1.GetType().GetProperty("Id")?.SetValue(chat1, Guid.NewGuid()); + chat1.AddMember(userId, ChatRole.Owner); + + var chat2 = Chat.Create("Personal", ChatType.Personal); + chat2.GetType().GetProperty("Id")?.SetValue(chat2, Guid.NewGuid()); + chat2.AddMember(userId, ChatRole.Member); + + _chatRepository.GetUserChatsAsync(userId, Arg.Any()) + .Returns(new List { chat1, chat2 }); + + _messageRepository.GetChatMessagesAsync(Arg.Any(), 1, 0, Arg.Any()) + .Returns(new List()); + + _userProvider.GetUsersInfoAsync(Arg.Any>(), Arg.Any()) + .Returns(new Dictionary()); + + // Act + var result = await _handler.Handle(request, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + + // It always appends synthetic "favorites" chat at the end if not found + result.Value.Count.Should().Be(2); + result.Value.Any(c => c.Name == "Test Chat").Should().BeTrue(); + result.Value.Any(c => c.Type == "favorites").Should().BeTrue(); + } +} + + diff --git a/backend/Tests/Conversations/UnitTests/GetOrCreateFavoritesCommandHandlerTests.cs b/backend/Tests/Conversations/UnitTests/GetOrCreateFavoritesCommandHandlerTests.cs new file mode 100644 index 0000000..0368f51 --- /dev/null +++ b/backend/Tests/Conversations/UnitTests/GetOrCreateFavoritesCommandHandlerTests.cs @@ -0,0 +1,74 @@ +using FluentAssertions; +using NSubstitute; +using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Modules.Conversations.Application.Chats.GetOrCreateFavorites; +using Knot.Modules.Conversations.Domain; +using Knot.Modules.Messaging.Domain; +using Knot.Shared.Kernel; +using Xunit; + +namespace Knot.Modules.Conversations.UnitTests; + +public class GetOrCreateFavoritesCommandHandlerTests +{ + private readonly IChatRepository _chatRepository; + private readonly IChatsUnitOfWork _unitOfWork; + private readonly GetOrCreateFavoritesCommandHandler _handler; + + public GetOrCreateFavoritesCommandHandlerTests() + { + _chatRepository = Substitute.For(); + _unitOfWork = Substitute.For(); + _handler = new GetOrCreateFavoritesCommandHandler(_chatRepository, _unitOfWork); + } + + [Fact] + public async Task Handle_ShouldReturnExistingChat_WhenFavoritesAlreadyExists() + { + // Arrange + var userId = Guid.NewGuid(); + var existingChat = Chat.Create("Избранное", ChatType.Favorites); + + _chatRepository.GetFavoritesAsync(userId, Arg.Any()) + .Returns(existingChat); + + var command = new GetOrCreateFavoritesCommand(userId); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Be(existingChat.Id); + + _chatRepository.DidNotReceive().Add(Arg.Any()); + await _unitOfWork.DidNotReceive().SaveChangesAsync(Arg.Any()); + } + + [Fact] + public async Task Handle_ShouldCreateNewChat_WhenFavoritesDoesNotExist() + { + // Arrange + var userId = Guid.NewGuid(); + _chatRepository.GetFavoritesAsync(userId, Arg.Any()) + .Returns((Chat)null!); + + var command = new GetOrCreateFavoritesCommand(userId); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeEmpty(); + + _chatRepository.Received(1).Add(Arg.Is(c => + c.Type == ChatType.Favorites && + c.Name == "Избранное" && + c.Members.Any(m => m.UserId == userId))); + + await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any()); + } +} + + diff --git a/backend/Tests/Conversations/UnitTests/Knot.Modules.Conversations.UnitTests.csproj b/backend/Tests/Conversations/UnitTests/Knot.Modules.Conversations.UnitTests.csproj new file mode 100644 index 0000000..11589b9 --- /dev/null +++ b/backend/Tests/Conversations/UnitTests/Knot.Modules.Conversations.UnitTests.csproj @@ -0,0 +1,33 @@ + + + + net10.0 + enable + enable + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + diff --git a/backend/Tests/Conversations/UnitTests/Messages/SendMessageCommandHandlerTests.cs b/backend/Tests/Conversations/UnitTests/Messages/SendMessageCommandHandlerTests.cs new file mode 100644 index 0000000..8e5192a --- /dev/null +++ b/backend/Tests/Conversations/UnitTests/Messages/SendMessageCommandHandlerTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using MediatR; +using NSubstitute; +using Xunit; +using Knot.Shared.Kernel; +using Knot.Modules.Conversations.Domain; +using Knot.Modules.Messaging.Domain; +using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Modules.Conversations.Application.Messages.Send; +using Knot.Modules.Settings.Application.Settings.Abstractions; +using Knot.Modules.Settings.Application.Settings.DTOs; + +namespace Knot.Modules.Conversations.UnitTests.Messages; + +public class SendMessageCommandHandlerTests +{ + private readonly IChatRepository _chatRepository; + private readonly IMessageRepository _messageRepository; + private readonly IChatsUnitOfWork _unitOfWork; + private readonly IMediator _mediator; + private readonly IMessagesSettings _messagesSettings; + private readonly SendMessageCommandHandler _handler; + + public SendMessageCommandHandlerTests() + { + _chatRepository = Substitute.For(); + _messageRepository = Substitute.For(); + _unitOfWork = Substitute.For(); + _mediator = Substitute.For(); + _messagesSettings = Substitute.For(); + + var config = new Knot.Modules.Settings.Application.Settings.DTOs.MessagesConfig(); + _messagesSettings.Current.Returns(config); + + _handler = new SendMessageCommandHandler(_chatRepository, _messageRepository, _unitOfWork, _mediator, _messagesSettings); + } + + [Fact] + public async Task Handle_ShouldReturnFailure_WhenChatNotFound() + { + // Arrange + var command = new SendMessageCommand(Guid.NewGuid(), Guid.NewGuid(), "Hello", "text"); + _chatRepository.GetByIdAsync(command.ChatId, Arg.Any()) + .Returns((Chat)null!); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Code.Should().Be(ChatErrors.ChatsNotFound.Code); + } + + [Fact] + public async Task Handle_ShouldReturnFailure_WhenSenderIsNotMember() + { + // Arrange + var chat = Chat.Create("Test", ChatType.Group); + var command = new SendMessageCommand(chat.Id, Guid.NewGuid(), "Hello", "text"); + + _chatRepository.GetByIdAsync(command.ChatId, Arg.Any()) + .Returns(chat); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Code.Should().Be(ChatErrors.ChatsForbidden.Code); + } + + [Fact] + public async Task Handle_ShouldCreateMessage_WhenAuthorized() + { + // Arrange + var senderId = Guid.NewGuid(); + var chat = Chat.Create("Test", ChatType.Group); + chat.AddMember(senderId, ChatRole.Member); + + var command = new SendMessageCommand(chat.Id, senderId, "Hello", "text"); + + _chatRepository.GetByIdAsync(command.ChatId, Arg.Any()) + .Returns(chat); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeEmpty(); + + _messageRepository.Received(1).Add(Arg.Is(m => + m.ChatId == chat.Id && + m.SenderId == senderId && + m.Content == "Hello" && + m.Type == "text")); + + await _unitOfWork.Received(1).SaveChangesAsync(Arg.Any()); + } +} + + diff --git a/backend/Tests/Federation/IntegrationTests/Knot.Modules.Federation.IntegrationTests.csproj b/backend/Tests/Federation/IntegrationTests/Knot.Modules.Federation.IntegrationTests.csproj new file mode 100644 index 0000000..1607c89 --- /dev/null +++ b/backend/Tests/Federation/IntegrationTests/Knot.Modules.Federation.IntegrationTests.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Federation/UnitTests/Knot.Modules.Federation.UnitTests.csproj b/backend/Tests/Federation/UnitTests/Knot.Modules.Federation.UnitTests.csproj new file mode 100644 index 0000000..c5416d2 --- /dev/null +++ b/backend/Tests/Federation/UnitTests/Knot.Modules.Federation.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Klipy/UnitTests/Knot.Modules.Klipy.UnitTests.csproj b/backend/Tests/Klipy/UnitTests/Knot.Modules.Klipy.UnitTests.csproj new file mode 100644 index 0000000..21ebf72 --- /dev/null +++ b/backend/Tests/Klipy/UnitTests/Knot.Modules.Klipy.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Messaging/IntegrationTests/Knot.Modules.Messaging.IntegrationTests.csproj b/backend/Tests/Messaging/IntegrationTests/Knot.Modules.Messaging.IntegrationTests.csproj new file mode 100644 index 0000000..1607c89 --- /dev/null +++ b/backend/Tests/Messaging/IntegrationTests/Knot.Modules.Messaging.IntegrationTests.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Messaging/UnitTests/Knot.Modules.Messaging.UnitTests.csproj b/backend/Tests/Messaging/UnitTests/Knot.Modules.Messaging.UnitTests.csproj new file mode 100644 index 0000000..89029aa --- /dev/null +++ b/backend/Tests/Messaging/UnitTests/Knot.Modules.Messaging.UnitTests.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Relations/IntegrationTests/Knot.Modules.Relations.IntegrationTests.csproj b/backend/Tests/Relations/IntegrationTests/Knot.Modules.Relations.IntegrationTests.csproj new file mode 100644 index 0000000..1607c89 --- /dev/null +++ b/backend/Tests/Relations/IntegrationTests/Knot.Modules.Relations.IntegrationTests.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Relations/UnitTests/Knot.Modules.Relations.UnitTests.csproj b/backend/Tests/Relations/UnitTests/Knot.Modules.Relations.UnitTests.csproj new file mode 100644 index 0000000..33cc70b --- /dev/null +++ b/backend/Tests/Relations/UnitTests/Knot.Modules.Relations.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Shared/IntegrationTests.Shared/BaseIntegrationTest.cs b/backend/Tests/Shared/IntegrationTests.Shared/BaseIntegrationTest.cs new file mode 100644 index 0000000..8645865 --- /dev/null +++ b/backend/Tests/Shared/IntegrationTests.Shared/BaseIntegrationTest.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System.Net.Http; +using Testcontainers.PostgreSql; +using Testcontainers.MongoDb; +using Xunit; +using System.Threading.Tasks; +using System.Collections.Generic; +using Knot.Shared.Kernel; +using NSubstitute; + +namespace Knot.IntegrationTests; + +public abstract class BaseIntegrationTest : IAsyncLifetime +{ + protected readonly PostgreSqlContainer _postgresContainer = new PostgreSqlBuilder() + .WithImage("postgres:15-alpine") + .Build(); + + protected readonly MongoDbContainer _mongoContainer = new MongoDbBuilder() + .WithImage("mongo:6.0") + .Build(); + + protected HttpClient _client; + protected WebApplicationFactory _factory; + protected readonly Guid _userId = Guid.NewGuid(); + protected readonly IUserContext _userContextMock = Substitute.For(); + + public virtual async Task InitializeAsync() + { + await _postgresContainer.StartAsync(); + await _mongoContainer.StartAsync(); + + _userContextMock.UserId.Returns(_userId); + _userContextMock.IsAuthenticated.Returns(true); + + _factory = new IntegrationTestWebApplicationFactory( + _postgresContainer.GetConnectionString(), + _mongoContainer.GetConnectionString(), + _userContextMock); + + _client = _factory.CreateClient(); + } + + public virtual async Task DisposeAsync() + { + await _postgresContainer.StopAsync(); + await _mongoContainer.StopAsync(); + _factory?.Dispose(); + _client?.Dispose(); + } + + private class IntegrationTestWebApplicationFactory : WebApplicationFactory + { + private readonly string _pgConnectionString; + private readonly string _mongoConnectionString; + private readonly IUserContext _userContext; + + public IntegrationTestWebApplicationFactory(string pg, string mongo, IUserContext userContext) + { + _pgConnectionString = pg; + _mongoConnectionString = mongo; + _userContext = userContext; + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureAppConfiguration((context, config) => + { + config.AddInMemoryCollection(new Dictionary + { + ["ConnectionStrings:DefaultConnection"] = _pgConnectionString, + ["ConnectionStrings:MongoConnection"] = _mongoConnectionString, + ["DATABASE_URL"] = _pgConnectionString, + ["MONGO_CONNECTION"] = _mongoConnectionString, + ["KNOT_MASTER_ENCRYPTION_KEY"] = "TestEncryptionKey_32CharactersLong!" + }); + }); + + builder.ConfigureTestServices(services => { + services.AddScoped(_ => _userContext); + }); + } + } +} diff --git a/backend/Tests/Shared/IntegrationTests.Shared/Knot.IntegrationTests.Shared.csproj b/backend/Tests/Shared/IntegrationTests.Shared/Knot.IntegrationTests.Shared.csproj new file mode 100644 index 0000000..b74a66f --- /dev/null +++ b/backend/Tests/Shared/IntegrationTests.Shared/Knot.IntegrationTests.Shared.csproj @@ -0,0 +1,38 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Storage/UnitTests/Knot.Modules.Storage.UnitTests.csproj b/backend/Tests/Storage/UnitTests/Knot.Modules.Storage.UnitTests.csproj new file mode 100644 index 0000000..2c59df3 --- /dev/null +++ b/backend/Tests/Storage/UnitTests/Knot.Modules.Storage.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Stories/IntegrationTests/Knot.Modules.Stories.IntegrationTests.csproj b/backend/Tests/Stories/IntegrationTests/Knot.Modules.Stories.IntegrationTests.csproj new file mode 100644 index 0000000..df3476d --- /dev/null +++ b/backend/Tests/Stories/IntegrationTests/Knot.Modules.Stories.IntegrationTests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Stories/IntegrationTests/StoriesTests.cs b/backend/Tests/Stories/IntegrationTests/StoriesTests.cs new file mode 100644 index 0000000..3a9d464 --- /dev/null +++ b/backend/Tests/Stories/IntegrationTests/StoriesTests.cs @@ -0,0 +1,34 @@ +using Knot.IntegrationTests; +using FluentAssertions; +using Knot.Modules.Stories.Application.Stories.Commands.CreateStory; +using Knot.Shared.Kernel; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using System.Net; +using System.Net.Http.Json; +using Xunit; +using System; +using System.Threading.Tasks; + +namespace Knot.IntegrationTests.Stories; + +public class StoriesTests : BaseIntegrationTest +{ + [Fact] + public async Task CreateStory_ShouldReturnOk_WhenValidRequest() + { + // Arrange + var request = new CreateStoryRequest("Text", null, "Hello Integration Test", null); + + // Act + var response = await _client.PostAsJsonAsync("api/stories", request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var content = await response.Content.ReadFromJsonAsync(); + } +} + +public record CreateStoryRequest(string Type, string? MediaUrl, string? Content, string? BgColor); diff --git a/backend/Tests/Stories/UnitTests/CreateStoryCommandHandlerTests.cs b/backend/Tests/Stories/UnitTests/CreateStoryCommandHandlerTests.cs new file mode 100644 index 0000000..604e657 --- /dev/null +++ b/backend/Tests/Stories/UnitTests/CreateStoryCommandHandlerTests.cs @@ -0,0 +1,76 @@ +using FluentAssertions; +using Knot.Modules.Stories.Application.Stories.Commands.CreateStory; +using Knot.Modules.Stories.Domain; +using Knot.Shared.Kernel; +using Knot.Modules.Settings.Application.Settings.Abstractions; +using Knot.Modules.Settings.Application.Settings.DTOs; +using Knot.Modules.Stories.Application.Abstractions; +using NSubstitute; +using Xunit; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Modules.Stories.UnitTests; + +public class CreateStoryCommandHandlerTests +{ + private readonly IStoryRepository _storyRepository; + private readonly ISettingsService _settingsService; + private readonly CreateStoryCommandHandler _handler; + + public CreateStoryCommandHandlerTests() + { + _storyRepository = Substitute.For(); + _settingsService = Substitute.For(); + _handler = new CreateStoryCommandHandler(_storyRepository, _settingsService); + } + + [Fact] + public async Task Handle_ShouldSucceed_WhenValidRequest() + { + // Arrange + var command = new CreateStoryCommand(Guid.NewGuid(), "Text", null, "Hello Story", null); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + await _storyRepository.Received(1).AddAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task Handle_ShouldFail_WhenKlipyDisabledAndKlipyType() + { + // Arrange + var command = new CreateStoryCommand(Guid.NewGuid(), "Klipy", "http://klipy.com/gif", null, null); + var settings = new SystemSettingsDto { Klipy = new KlipyConfig { Enabled = false } }; + _settingsService.GetSettingsAsync(Arg.Any()).Returns(settings); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Code.Should().Be("Stories.KlipyDisabled"); + await _storyRepository.DidNotReceive().AddAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task Handle_ShouldSucceed_WhenKlipyEnabledAndKlipyType() + { + // Arrange + var command = new CreateStoryCommand(Guid.NewGuid(), "Klipy", "http://klipy.com/gif", null, null); + var settings = new SystemSettingsDto { Klipy = new KlipyConfig { Enabled = true } }; + _settingsService.GetSettingsAsync(Arg.Any()).Returns(settings); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + await _storyRepository.Received(1).AddAsync(Arg.Any(), Arg.Any()); + } +} + diff --git a/backend/Tests/Stories/UnitTests/DeleteStoryCommandHandlerTests.cs b/backend/Tests/Stories/UnitTests/DeleteStoryCommandHandlerTests.cs new file mode 100644 index 0000000..48795ad --- /dev/null +++ b/backend/Tests/Stories/UnitTests/DeleteStoryCommandHandlerTests.cs @@ -0,0 +1,76 @@ +using FluentAssertions; +using Knot.Modules.Stories.Application.Stories.Commands.DeleteStory; +using Knot.Modules.Stories.Domain; +using Knot.Shared.Kernel; +using Knot.Modules.Stories.Application.Abstractions; +using NSubstitute; +using Xunit; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Modules.Stories.UnitTests; + +public class DeleteStoryCommandHandlerTests +{ + private readonly IStoryRepository _storyRepository; + private readonly DeleteStoryCommandHandler _handler; + + public DeleteStoryCommandHandlerTests() + { + _storyRepository = Substitute.For(); + _handler = new DeleteStoryCommandHandler(_storyRepository); + } + + [Fact] + public async Task Handle_ShouldReturnError_WhenStoryNotFound() + { + // Arrange + var command = new DeleteStoryCommand(Guid.NewGuid(), Guid.NewGuid()); + _storyRepository.GetByIdAsync(command.StoryId, Arg.Any()).Returns((Story?)null); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Should().Be(StoryErrors.StoryNotFound); + } + + [Fact] + public async Task Handle_ShouldReturnError_WhenUserIsNotOwner() + { + // Arrange + var ownerId = Guid.NewGuid(); + var viewerId = Guid.NewGuid(); + var story = Story.Create(ownerId, StoryType.Text, null, "Content", null); + var command = new DeleteStoryCommand(viewerId, story.Id); + + _storyRepository.GetByIdAsync(command.StoryId, Arg.Any()).Returns(story); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Should().Be(StoryErrors.Unauthorized); + } + + [Fact] + public async Task Handle_ShouldSucceed_WhenOwnerDeletes() + { + // Arrange + var ownerId = Guid.NewGuid(); + var story = Story.Create(ownerId, StoryType.Text, null, "Content", null); + var command = new DeleteStoryCommand(ownerId, story.Id); + + _storyRepository.GetByIdAsync(command.StoryId, Arg.Any()).Returns(story); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + await _storyRepository.Received(1).DeleteAsync(story, Arg.Any()); + } +} diff --git a/backend/Tests/Stories/UnitTests/Knot.Modules.Stories.UnitTests.csproj b/backend/Tests/Stories/UnitTests/Knot.Modules.Stories.UnitTests.csproj new file mode 100644 index 0000000..71d70a1 --- /dev/null +++ b/backend/Tests/Stories/UnitTests/Knot.Modules.Stories.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/TelegramImport/UnitTests/Knot.Modules.TelegramImport.UnitTests.csproj b/backend/Tests/TelegramImport/UnitTests/Knot.Modules.TelegramImport.UnitTests.csproj new file mode 100644 index 0000000..bf92713 --- /dev/null +++ b/backend/Tests/TelegramImport/UnitTests/Knot.Modules.TelegramImport.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Users/UnitTests/Knot.Modules.Profiles.UnitTests.csproj b/backend/Tests/Users/UnitTests/Knot.Modules.Profiles.UnitTests.csproj new file mode 100644 index 0000000..3e3dac3 --- /dev/null +++ b/backend/Tests/Users/UnitTests/Knot.Modules.Profiles.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/Tests/Users/UnitTests/UpdateProfileCommandHandlerTests.cs b/backend/Tests/Users/UnitTests/UpdateProfileCommandHandlerTests.cs new file mode 100644 index 0000000..3739a53 --- /dev/null +++ b/backend/Tests/Users/UnitTests/UpdateProfileCommandHandlerTests.cs @@ -0,0 +1,38 @@ +using FluentAssertions; +using Knot.Modules.Profiles.Application.Profiles.UpdateProfile; +using Knot.Modules.Profiles.Domain; +using Knot.Shared.Kernel; +using NSubstitute; +using Xunit; +using System; +using System.Threading; +using System.Threading.Tasks; +using Knot.Modules.Profiles.Application.Abstractions; + +namespace Knot.Modules.Profiles.UnitTests; + +public class UpdateProfileCommandHandlerTests +{ + private readonly IProfileRepository _profileRepository; + private readonly UpdateProfileCommandHandler _handler; + + public UpdateProfileCommandHandlerTests() + { + _profileRepository = Substitute.For(); + _handler = new UpdateProfileCommandHandler(_profileRepository); + } + + [Fact] + public async Task Handle_ShouldReturnError_WhenProfileNotFound() + { + // Arrange + var command = new UpdateProfileCommand(Guid.NewGuid(), "FirstName", "Bio", null); + _profileRepository.GetByIdAsync(command.UserId, Arg.Any()).Returns((ProfileDocument?)null); + + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailure.Should().BeTrue(); + } +} diff --git a/backend/Tests/WebRtc/UnitTests/Knot.Modules.WebRtc.UnitTests.csproj b/backend/Tests/WebRtc/UnitTests/Knot.Modules.WebRtc.UnitTests.csproj new file mode 100644 index 0000000..be78aa8 --- /dev/null +++ b/backend/Tests/WebRtc/UnitTests/Knot.Modules.WebRtc.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/Modules/Auth/Application/Abstractions/IIdentityDbContext.cs b/backend/src/Modules/Auth/Application/Abstractions/IIdentityDbContext.cs new file mode 100644 index 0000000..a323aa8 --- /dev/null +++ b/backend/src/Modules/Auth/Application/Abstractions/IIdentityDbContext.cs @@ -0,0 +1,14 @@ +using System.Threading; +using System.Threading.Tasks; +using Knot.Modules.Auth.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Knot.Modules.Auth.Application.Abstractions; + +public interface IAuthDbContext +{ + DbSet Users { get; } + Task SaveChangesAsync(CancellationToken cancellationToken); +} + diff --git a/backend/src/Modules/Auth/Application/Abstractions/IIdentityUnitOfWork.cs b/backend/src/Modules/Auth/Application/Abstractions/IIdentityUnitOfWork.cs new file mode 100644 index 0000000..11f260e --- /dev/null +++ b/backend/src/Modules/Auth/Application/Abstractions/IIdentityUnitOfWork.cs @@ -0,0 +1,11 @@ +using Knot.Shared.Kernel; + +namespace Knot.Modules.Auth.Application.Abstractions; + +/// +/// Unit of Work специфичный для модуля Identity. +/// +public interface IAuthUnitOfWork : IUnitOfWork +{ +} + diff --git a/backend/src/Modules/Auth/Application/Abstractions/IJwtTokenProvider.cs b/backend/src/Modules/Auth/Application/Abstractions/IJwtTokenProvider.cs new file mode 100644 index 0000000..4cf048e --- /dev/null +++ b/backend/src/Modules/Auth/Application/Abstractions/IJwtTokenProvider.cs @@ -0,0 +1,9 @@ +using Knot.Modules.Auth.Domain; + +namespace Knot.Modules.Auth.Application.Abstractions; + +public interface IJwtTokenProvider +{ + string Generate(User user); +} + diff --git a/backend/src/Modules/Auth/Application/Auth/DTOs/AuthResponseDto.cs b/backend/src/Modules/Auth/Application/Auth/DTOs/AuthResponseDto.cs new file mode 100644 index 0000000..4e28fd4 --- /dev/null +++ b/backend/src/Modules/Auth/Application/Auth/DTOs/AuthResponseDto.cs @@ -0,0 +1,21 @@ +using System; + +namespace Knot.Modules.Auth.Application.Auth.DTOs; + +public record AuthResponseDto( + string Token, + AuthUserDto User +); + +public record AuthUserDto( + Guid Id, + string Username, + string DisplayName, + string? Email, + string? Bio, + string? Avatar, + DateTime? Birthday, + bool IsOnline, + DateTime CreatedAt +); + diff --git a/backend/src/Modules/Auth/Application/Users/Auth/AuthResponseDto.cs b/backend/src/Modules/Auth/Application/Users/Auth/AuthResponseDto.cs new file mode 100644 index 0000000..17f2752 --- /dev/null +++ b/backend/src/Modules/Auth/Application/Users/Auth/AuthResponseDto.cs @@ -0,0 +1,21 @@ +using System; + +namespace Knot.Modules.Auth.Application.Users.Auth; + +public record AuthResponseDto( + string Token, + AuthUserDto User +); + +public record AuthUserDto( + Guid Id, + string Username, + string DisplayName, + string? Email, + string? Bio, + string? Avatar, + DateTime? Birthday, + bool IsOnline, + DateTime CreatedAt +); + diff --git a/backend/src/Modules/Auth/Domain/AuthErrors.cs b/backend/src/Modules/Auth/Domain/AuthErrors.cs new file mode 100644 index 0000000..8361373 --- /dev/null +++ b/backend/src/Modules/Auth/Domain/AuthErrors.cs @@ -0,0 +1,15 @@ +using Knot.Shared.Kernel; + +namespace Knot.Modules.Auth.Domain; + +public static class AuthErrors +{ + public static readonly Error FriendsNotFound = new Error("Friends.NotFound", "Friendship not found"); + public static readonly Error FriendsSelf = new Error("Friends.Self", "Cannot add yourself"); + public static readonly Error FriendsExists = new Error("Friends.Exists", "Friendship already exists"); + public static readonly Error UserNotFound = new Error("User.NotFound", "User not found"); + public static readonly Error IdentityInvalidCredentials = new Error("Identity.InvalidCredentials", "Неверное имя пользователя или пароль."); + public static readonly Error IdentityRegistrationDisabled = new Error("Identity.RegistrationDisabled", "Registration is disabled by the administrator."); + public static readonly Error IdentityUsernameNotUnique = new Error("Identity.UsernameNotUnique", "Это имя пользователя уже занято."); +} + diff --git a/backend/src/Modules/Auth/Domain/IUserRepository.cs b/backend/src/Modules/Auth/Domain/IUserRepository.cs new file mode 100644 index 0000000..bf25a49 --- /dev/null +++ b/backend/src/Modules/Auth/Domain/IUserRepository.cs @@ -0,0 +1,19 @@ +using Knot.Modules.Auth.Domain; + +namespace Knot.Modules.Auth.Domain; + +/// +/// Интерфейс репозитория для работы с пользователями. +/// +public interface IUserRepository +{ + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task> GetByIdsAsync(IEnumerable ids, CancellationToken cancellationToken = default); + Task GetByUsernameAsync(string username, CancellationToken cancellationToken = default); + Task IsUsernameUniqueAsync(string username, CancellationToken cancellationToken = default); + Task> SearchUsersAsync(string query, CancellationToken cancellationToken = default); + void Add(User user); + void Update(User user); + void Remove(User user); +} + diff --git a/backend/src/Modules/Messaging/Infrastructure/Persistence/Mongo/MongoDbMapConfigurator.cs b/backend/src/Modules/Messaging/Infrastructure/Persistence/Mongo/MongoDbMapConfigurator.cs index 97ed335..693f229 100644 --- a/backend/src/Modules/Messaging/Infrastructure/Persistence/Mongo/MongoDbMapConfigurator.cs +++ b/backend/src/Modules/Messaging/Infrastructure/Persistence/Mongo/MongoDbMapConfigurator.cs @@ -61,8 +61,8 @@ public static class MongoDbMapConfigurator BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); - cm.MapField("_options").SetElementName("Options"); - cm.MapField("_votes").SetElementName("Votes"); + cm.MapProperty(c => c.Options).SetElementName("_options"); + cm.MapProperty(c => c.Votes).SetElementName("_votes"); cm.SetDiscriminator("PollMessage"); }); diff --git a/backend/src/Modules/Profiles/Domain/IAvatarStorageService.cs b/backend/src/Modules/Profiles/Domain/IAvatarStorageService.cs new file mode 100644 index 0000000..96f5192 --- /dev/null +++ b/backend/src/Modules/Profiles/Domain/IAvatarStorageService.cs @@ -0,0 +1,11 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Modules.Profiles.Domain; + +public interface IAvatarStorageService +{ + Task UploadAsync(Stream content, string fileName, string contentType, CancellationToken ct = default); + Task DeleteAsync(string fileId, CancellationToken ct = default); +} diff --git a/backend/src/Modules/Profiles/Domain/IProfileRepository.cs b/backend/src/Modules/Profiles/Domain/IProfileRepository.cs new file mode 100644 index 0000000..74f365a --- /dev/null +++ b/backend/src/Modules/Profiles/Domain/IProfileRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Modules.Profiles.Domain; + +public interface IProfileRepository +{ + Task GetByIdAsync(Guid id, CancellationToken ct = default); + Task GetByUsernameAsync(string username, CancellationToken ct = default); + Task AddAsync(ProfileDocument profile, CancellationToken ct = default); + Task UpdateAsync(ProfileDocument profile, CancellationToken ct = default); + Task> SearchAsync(string query, CancellationToken ct = default); +} diff --git a/backend/src/Modules/Profiles/Domain/IProfilesUnitOfWork.cs b/backend/src/Modules/Profiles/Domain/IProfilesUnitOfWork.cs new file mode 100644 index 0000000..b18ed41 --- /dev/null +++ b/backend/src/Modules/Profiles/Domain/IProfilesUnitOfWork.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Modules.Profiles.Domain; + +public interface IProfilesUnitOfWork +{ + Task SaveChangesAsync(CancellationToken ct = default); +} diff --git a/backend/src/Modules/Profiles/Domain/ProfilesErrors.cs b/backend/src/Modules/Profiles/Domain/ProfilesErrors.cs new file mode 100644 index 0000000..23bc1be --- /dev/null +++ b/backend/src/Modules/Profiles/Domain/ProfilesErrors.cs @@ -0,0 +1,10 @@ +namespace Knot.Modules.Profiles.Domain; +using Knot.Shared.Kernel; +public static class ProfilesErrors +{ + public static readonly Error ProfileNotFound = new("Profile.NotFound", "Profile not found"); +} +public static class IdentityErrors +{ + public static readonly Error UserNotFound = new("Profile.NotFound", "Profile not found"); +} diff --git a/backend/src/Modules/Settings/Application/Settings/Abstractions/ISettingsService.cs b/backend/src/Modules/Settings/Application/Settings/Abstractions/ISettingsService.cs new file mode 100644 index 0000000..46cef1d --- /dev/null +++ b/backend/src/Modules/Settings/Application/Settings/Abstractions/ISettingsService.cs @@ -0,0 +1,52 @@ +using System.Threading; +using System.Threading.Tasks; +using Knot.Modules.Settings.Application.Settings.DTOs; + +namespace Knot.Modules.Settings.Application.Settings.Abstractions; + +public interface ISettingsService +{ + Task GetSettingsAsync(CancellationToken cancellationToken = default); + Task UpdateSettingsAsync(SystemSettingsDto settings, CancellationToken cancellationToken = default); + SystemSettingsDto Current { get; } +} + +public interface ISystemSettings +{ + SystemConfig Current { get; } +} + +public interface IStoriesSettings +{ + StoriesConfig Current { get; } +} + +public interface IChatsSettings +{ + ChatsConfig Current { get; } +} + +public interface IMessagesSettings +{ + MessagesConfig Current { get; } +} + +public interface IWebRtcSettings +{ + WebRtcConfig Current { get; } +} + +public interface IKlipySettings +{ + KlipyConfig Current { get; } +} + +public interface IImportSettings +{ + ImportConfig Current { get; } +} + +public interface IFederationSettings +{ + FederationConfig Current { get; } +} diff --git a/backend/src/Modules/Settings/Application/Settings/DTOs/PublicConfigDto.cs b/backend/src/Modules/Settings/Application/Settings/DTOs/PublicConfigDto.cs new file mode 100644 index 0000000..7fc200a --- /dev/null +++ b/backend/src/Modules/Settings/Application/Settings/DTOs/PublicConfigDto.cs @@ -0,0 +1,159 @@ +using System.Collections.Generic; +using Knot.Modules.Settings.Application.Settings.DTOs; + +namespace Knot.Modules.Settings.Application.Settings.DTOs; + +public record PublicConfigDto +{ + public SystemConfigDto System { get; init; } = new(); + public StoriesConfigDto Stories { get; init; } = new(); + public ChatsConfigDto Chats { get; init; } = new(); + public MessagesConfigDto Messages { get; init; } = new(); + public WebRtcConfigDto WebRtc { get; init; } = new(); + public KlipyConfigDto Klipy { get; init; } = new(); + public ImportConfigDto Import { get; init; } = new(); + public FederationConfigDto Federation { get; init; } = new(); + + public static PublicConfigDto FromSettings(SystemSettingsDto settings) + { + return new PublicConfigDto + { + System = new SystemConfigDto + { + DomainUrl = settings.System.DomainUrl, + EnableRegistration = settings.System.EnableRegistration + }, + Stories = new StoriesConfigDto + { + Enabled = settings.Stories.Enabled, + MaxStoriesPerPeriod = settings.Stories.MaxStoriesPerPeriod, + StoryLifetimeHours = settings.Stories.StoryLifetimeHours, + TextStoriesEnabled = settings.Stories.TextStoriesEnabled, + TextStoryDurationSeconds = settings.Stories.TextStoryDurationSeconds, + MediaStoryMaxDurationSeconds = settings.Stories.MediaStoryMaxDurationSeconds, + MaxMediaSizeBytes = settings.Stories.MaxMediaSizeBytes + }, + Chats = new ChatsConfigDto + { + SupportGroups = settings.Chats.SupportGroups, + MaxGroupParticipants = settings.Chats.MaxGroupParticipants, + AllowChatToGroupConversion = settings.Chats.AllowChatToGroupConversion, + EnableFolders = settings.Chats.EnableFolders + }, + Messages = new MessagesConfigDto + { + DailyMessageLimitPerUser = settings.Messages.DailyMessageLimitPerUser, + ChatMessageLimit = settings.Messages.ChatMessageLimit, + AllowMedia = settings.Messages.AllowMedia, + MaxMediaSizeBytes = settings.Messages.MaxMediaSizeBytes, + AllowedMediaTypes = settings.Messages.AllowedMediaTypes, + AllowVoiceMessages = settings.Messages.AllowVoiceMessages, + AllowForwarding = settings.Messages.AllowForwarding, + AllowReactions = settings.Messages.AllowReactions, + AllowReplies = settings.Messages.AllowReplies, + AllowQuoting = settings.Messages.AllowQuoting, + AllowMessageDeletion = settings.Messages.AllowMessageDeletion, + ForbidCopying = settings.Messages.ForbidCopying, + AllowLinks = settings.Messages.AllowLinks, + AllowPolls = settings.Messages.AllowPolls, + AllowPinning = settings.Messages.AllowPinning + }, + WebRtc = new WebRtcConfigDto + { + Enabled = settings.WebRtc.Enabled, + EnableVoiceCalls = settings.WebRtc.EnableVoiceCalls, + EnableVideoCalls = settings.WebRtc.EnableVideoCalls, + EnableScreenSharing = settings.WebRtc.EnableScreenSharing, + TurnHost = settings.WebRtc.TurnHost, + TurnPort = settings.WebRtc.TurnPort + }, + Klipy = new KlipyConfigDto + { + Enabled = settings.Klipy.Enabled, + AppName = settings.Klipy.AppName + }, + Import = new ImportConfigDto + { + Enabled = settings.Import.EnableTelegramImport + }, + Federation = new FederationConfigDto + { + Enabled = settings.Federation.Enabled, + ServerDescription = settings.Federation.ServerDescription, + AllowedDomains = settings.Federation.AllowedDomains + } + }; + } +} + +public record SystemConfigDto +{ + public string DomainUrl { get; init; } = string.Empty; + public bool EnableRegistration { get; init; } +} + +public record StoriesConfigDto +{ + public bool Enabled { get; init; } + public int MaxStoriesPerPeriod { get; init; } + public int StoryLifetimeHours { get; init; } + public bool TextStoriesEnabled { get; init; } + public int TextStoryDurationSeconds { get; init; } + public int MediaStoryMaxDurationSeconds { get; init; } + public int MaxMediaSizeBytes { get; init; } +} + +public record ChatsConfigDto +{ + public bool SupportGroups { get; init; } + public int MaxGroupParticipants { get; init; } + public bool AllowChatToGroupConversion { get; init; } + public bool EnableFolders { get; init; } +} + +public record MessagesConfigDto +{ + public int DailyMessageLimitPerUser { get; init; } + public int ChatMessageLimit { get; init; } + public bool AllowMedia { get; init; } + public int MaxMediaSizeBytes { get; init; } + public List AllowedMediaTypes { get; init; } = new(); + public bool AllowVoiceMessages { get; init; } + public bool AllowForwarding { get; init; } + public bool AllowReactions { get; init; } + public bool AllowReplies { get; init; } + public bool AllowQuoting { get; init; } + public bool AllowMessageDeletion { get; init; } + public bool ForbidCopying { get; init; } + public bool AllowLinks { get; init; } + public bool AllowPolls { get; init; } + public bool AllowPinning { get; init; } +} + +public record WebRtcConfigDto +{ + public bool Enabled { get; init; } + public bool EnableVoiceCalls { get; init; } + public bool EnableVideoCalls { get; init; } + public bool EnableScreenSharing { get; init; } + public string TurnHost { get; init; } = string.Empty; + public int TurnPort { get; init; } +} + +public record KlipyConfigDto +{ + public bool Enabled { get; init; } + public string AppName { get; init; } = string.Empty; +} + +public record ImportConfigDto +{ + public bool Enabled { get; init; } +} + +public record FederationConfigDto +{ + public bool Enabled { get; init; } + public string ServerDescription { get; init; } = string.Empty; + public List AllowedDomains { get; init; } = new(); +} diff --git a/backend/src/Modules/Settings/Application/Settings/DTOs/SystemSettingsDto.cs b/backend/src/Modules/Settings/Application/Settings/DTOs/SystemSettingsDto.cs new file mode 100644 index 0000000..d34cfaf --- /dev/null +++ b/backend/src/Modules/Settings/Application/Settings/DTOs/SystemSettingsDto.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; + +namespace Knot.Modules.Settings.Application.Settings.DTOs; + +public class SystemConfig +{ + public string ServerTimezone { get; set; } = "UTC"; + public string DomainUrl { get; set; } = "https://example.com"; + public string AdminRoute { get; set; } = "admin"; + public bool EnableRegistration { get; set; } = true; +} + +public class StoriesConfig +{ + public bool Enabled { get; set; } = true; + public int MaxStoriesPerPeriod { get; set; } = 5; + public int StoryLifetimeHours { get; set; } = 24; + public bool TextStoriesEnabled { get; set; } = true; + public int TextStoryDurationSeconds { get; set; } = 15; + public int MediaStoryMaxDurationSeconds { get; set; } = 30; + public int MaxMediaSizeBytes { get; set; } = 15 * 1024 * 1024; +} + +public class ChatsConfig +{ + public bool SupportGroups { get; set; } = true; + public int MaxGroupParticipants { get; set; } = 200000; + public bool AutoCleanChats { get; set; } = false; + public bool AllowChatToGroupConversion { get; set; } = true; + public bool EnableFolders { get; set; } = true; +} + +public class MessagesConfig +{ + public int DailyMessageLimitPerUser { get; set; } = 0; + public int ChatMessageLimit { get; set; } = 0; + public bool AllowMedia { get; set; } = true; + public int MaxMediaSizeBytes { get; set; } = 50 * 1024 * 1024; + public List AllowedMediaTypes { get; set; } = new() { "image/jpeg", "image/png", "video/mp4", "image/gif" }; + public bool AllowVoiceMessages { get; set; } = true; + public bool AllowForwarding { get; set; } = true; + public bool AllowReactions { get; set; } = true; + public bool AllowReplies { get; set; } = true; + public bool AllowQuoting { get; set; } = true; + public bool AllowMessageDeletion { get; set; } = true; + public bool ForbidCopying { get; set; } = false; + public bool AllowLinks { get; set; } = true; + public bool AllowPolls { get; set; } = true; + public bool AllowPinning { get; set; } = true; +} + +public class WebRtcConfig +{ + public bool Enabled { get; set; } = false; + public bool EnableVoiceCalls { get; set; } = true; + public bool EnableVideoCalls { get; set; } = true; + public bool EnableScreenSharing { get; set; } = true; + public string TurnHost { get; set; } = string.Empty; + public int TurnPort { get; set; } = 3478; + public string TurnUser { get; set; } = string.Empty; + public string TurnSecret { get; set; } = string.Empty; +} + +public class KlipyConfig +{ + public bool Enabled { get; set; } = false; + public string ApiKey { get; set; } = string.Empty; + public string AppName { get; set; } = string.Empty; +} + +public class ImportConfig +{ + public bool EnableTelegramImport { get; set; } = false; +} + +public class FederationDomainConfig +{ + public string Domain { get; set; } = string.Empty; + public bool IsEnabled { get; set; } = true; + public string? PublicKey { get; set; } + public RemoteCapabilities? Capabilities { get; set; } +} + +public class RemoteCapabilities +{ + public bool AllowMedia { get; set; } + public bool AllowPolls { get; set; } + public bool AllowVoiceMessages { get; set; } + public bool AllowVideoCalls { get; set; } + public bool AllowScreenSharing { get; set; } +} + +public class FederationConfig +{ + public bool Enabled { get; set; } = false; + public string ServerDescription { get; set; } = string.Empty; + public string? PrivateKey { get; set; } + public string? PublicKey { get; set; } + public List AllowedDomains { get; set; } = new(); +} + +public class SystemSettingsDto +{ + public SystemConfig System { get; set; } = new(); + public StoriesConfig Stories { get; set; } = new(); + public ChatsConfig Chats { get; set; } = new(); + public MessagesConfig Messages { get; set; } = new(); + public WebRtcConfig WebRtc { get; set; } = new(); + public KlipyConfig Klipy { get; set; } = new(); + public ImportConfig Import { get; set; } = new(); + public FederationConfig Federation { get; set; } = new(); +} diff --git a/backend/src/Modules/Stories/Application/Abstractions/IKlipyClient.cs b/backend/src/Modules/Stories/Application/Abstractions/IKlipyClient.cs new file mode 100644 index 0000000..9a06e0d --- /dev/null +++ b/backend/src/Modules/Stories/Application/Abstractions/IKlipyClient.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Modules.Stories.Application.Abstractions; + +public interface IKlipyClient +{ + Task TestConnectionAsync(string apiKey, CancellationToken ct = default); + Task> SearchVideosAsync(string apiKey, string query, int limit, CancellationToken ct = default); +} diff --git a/backend/src/Modules/Stories/Application/Abstractions/IStoriesDbContext.cs b/backend/src/Modules/Stories/Application/Abstractions/IStoriesDbContext.cs new file mode 100644 index 0000000..777b5f5 --- /dev/null +++ b/backend/src/Modules/Stories/Application/Abstractions/IStoriesDbContext.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Knot.Modules.Stories.Domain; +using Knot.Modules.Relations.Domain; + +namespace Knot.Modules.Stories.Application.Abstractions; + +public interface IStoriesDbContext +{ + DbSet Stories { get; } + DbSet Friendships { get; } + DbSet StoryViewers { get; } + DbSet Set() where TEntity : class; + Task SaveChangesAsync(CancellationToken cancellationToken); +} + +public interface IStoriesUnitOfWork +{ + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/backend/src/Modules/Stories/Infrastructure/External/KlipyClient.cs b/backend/src/Modules/Stories/Infrastructure/External/KlipyClient.cs new file mode 100644 index 0000000..aad4588 --- /dev/null +++ b/backend/src/Modules/Stories/Infrastructure/External/KlipyClient.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Knot.Modules.Stories.Application.Abstractions; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Modules.Stories.Infrastructure.External; + +public sealed class KlipyClient : IKlipyClient +{ + private readonly HttpClient _httpClient; + + public KlipyClient(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public async Task TestConnectionAsync(string apiKey, CancellationToken ct = default) + { + try + { + var request = new HttpRequestMessage(HttpMethod.Get, "https://api.klipy.co/v1/trending?limit=1"); + request.Headers.Add("X-API-KEY", apiKey); + + using var response = await _httpClient.SendAsync(request, ct); + return response.IsSuccessStatusCode; + } + catch + { + return false; + } + } + + public async Task> SearchVideosAsync(string apiKey, string query, int limit, CancellationToken ct = default) + { + // В реальном проекте: десериализация ответа от Klipy API + return new List(); + } +} diff --git a/client-web/src/modules/stories/infrastructure/storyApi.ts b/client-web/src/modules/stories/infrastructure/storyApi.ts index 20c196b..8bdfe72 100644 --- a/client-web/src/modules/stories/infrastructure/storyApi.ts +++ b/client-web/src/modules/stories/infrastructure/storyApi.ts @@ -47,9 +47,8 @@ export class StoryApi { } static async removeStoryReaction(storyId: string, emoji: string) { - return httpClient.request<{ message: string }>(`/stories/${storyId}/reaction`, { - method: 'DELETE', - body: JSON.stringify({ emoji }), + return httpClient.request<{ message: string }>(`/stories/${storyId}/reaction/delete?emoji=${encodeURIComponent(emoji)}`, { + method: 'POST', }); }