diff --git a/Govor.API.Tests/IntegrationTests/Controllers/MediaControllerTests.cs b/Govor.API.Tests/IntegrationTests/Controllers/MediaControllerTests.cs index fcb5e4c..9f80ed2 100644 --- a/Govor.API.Tests/IntegrationTests/Controllers/MediaControllerTests.cs +++ b/Govor.API.Tests/IntegrationTests/Controllers/MediaControllerTests.cs @@ -18,6 +18,7 @@ public class MediaControllerTests private Mock _currentUserMock; private Mock> _loggerMock; private Mock _mockMedia; + private Mock _mockAccesser; private MediaController _controller; private Guid _userId = Guid.NewGuid(); @@ -30,11 +31,16 @@ public class MediaControllerTests _currentUserMock = new Mock(); _loggerMock = new Mock>(); + _mockAccesser = new Mock(); _mockMedia = new Mock(); _currentUserMock.Setup(f => f.GetCurrentUserId()).Returns(_userId); - _controller = new MediaController(_loggerMock.Object, _mockMedia.Object, _currentUserMock.Object); + _controller = new MediaController( + _loggerMock.Object, + _mockMedia.Object, + _mockAccesser.Object, + _currentUserMock.Object); } // Test for Upload action diff --git a/Govor.API/Controllers/MediaController.cs b/Govor.API/Controllers/MediaController.cs index 4768b0f..d9ada27 100644 --- a/Govor.API/Controllers/MediaController.cs +++ b/Govor.API/Controllers/MediaController.cs @@ -1,9 +1,6 @@ -using Govor.Application.Interfaces; using Govor.Application.Interfaces.Infrastructure.Extensions; using Govor.Application.Interfaces.Medias; using Govor.Contracts.Requests; -using Govor.Core.Models; -using Govor.Core.Repositories.MediasAttachments; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -16,14 +13,17 @@ public class MediaController : Controller { private readonly ILogger _logger; private readonly IMediaService _mediaService; + private readonly IAccesserToDownloadMedia _accesser; private readonly ICurrentUserService _currentUserService; public MediaController( ILogger logger, IMediaService mediaService, + IAccesserToDownloadMedia accesser, ICurrentUserService currentUserService) { _logger = logger; + _accesser = accesser; _mediaService = mediaService; _currentUserService = currentUserService; } @@ -32,20 +32,21 @@ public class MediaController : Controller [RequestSizeLimit(20_000_000)] // ~20MB public async Task 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 { - if (request.FromFile.Length > 20_000_000) - return BadRequest("File is too large"); - - if (!ModelState.IsValid) - return BadRequest(ModelState); - - // Чтение байт из IFormFile - using var memoryStream = new MemoryStream(); - - await request.FromFile.CopyToAsync(memoryStream); - - byte[] fileBytes = memoryStream.ToArray(); + byte[] fileBytes = await ReadFileAsync(request.FromFile); var media = new Media( _currentUserService.GetCurrentUserId(), @@ -59,21 +60,21 @@ public class MediaController : Controller var result = await _mediaService.UploadMediaAsync(media); - _logger.LogInformation( - $"Uploaded file: {Path.GetFileName(request.FromFile.FileName)} from user {_currentUserService.GetCurrentUserId()}"); + _logger.LogInformation("Uploaded file {FileName} from user {UserId}", + media.FileName, media.UploaderId); return Ok(result); } - catch (InvalidOperationException ex) - { - _logger.LogWarning(ex, ex.Message); - return BadRequest(ex.Message); - } catch (UnauthorizedAccessException ex) { _logger.LogWarning(ex, ex.Message); return Unauthorized(ex.Message); } + catch (InvalidOperationException ex) + { + _logger.LogWarning(ex, ex.Message); + return BadRequest(ex.Message); + } catch (Exception ex) { _logger.LogError(ex, "Error uploading media"); @@ -81,21 +82,32 @@ public class MediaController : Controller } } + private async Task ReadFileAsync(IFormFile file) + { + await using var ms = new MemoryStream(); + await file.CopyToAsync(ms); + return ms.ToArray(); + } + + [HttpGet("download/{id}")] public async Task Download(Guid id) { try { - if (!ModelState.IsValid) - return BadRequest(ModelState); + var userId = _currentUserService.GetCurrentUserId(); + + if (!await _accesser.HasAccessAsync(id, userId)) + return Forbid(); var media = await _mediaService.GetMediaByIdAsync(id); + return File(media.Data, media.MineType, Path.GetFileName(media.FileName)); } catch (KeyNotFoundException ex) { _logger.LogWarning(ex, ex.Message); - return NotFound(ex.Message); + return NotFound("Media not found"); } catch (Exception ex) { diff --git a/Govor.API/Extensions/ConfigurationProgramExtensions.cs b/Govor.API/Extensions/ConfigurationProgramExtensions.cs index d7ced36..d169afb 100644 --- a/Govor.API/Extensions/ConfigurationProgramExtensions.cs +++ b/Govor.API/Extensions/ConfigurationProgramExtensions.cs @@ -11,6 +11,7 @@ using Govor.Application.Interfaces.UserSession; 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.UserSessions; using Govor.Core.Infrastructure.Extensions; @@ -68,6 +69,7 @@ public static class ConfigurationProgramExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // UserSession services.AddScoped(); diff --git a/Govor.Application.Tests/Govor.Application.Tests.csproj b/Govor.Application.Tests/Govor.Application.Tests.csproj index cdd654f..b9c4e8e 100644 --- a/Govor.Application.Tests/Govor.Application.Tests.csproj +++ b/Govor.Application.Tests/Govor.Application.Tests.csproj @@ -27,7 +27,7 @@ - + diff --git a/Govor.Application.Tests/Validators/UsernameValidatorTests.cs b/Govor.Application.Tests/Infrastructure/Validators/UsernameValidatorTests.cs similarity index 95% rename from Govor.Application.Tests/Validators/UsernameValidatorTests.cs rename to Govor.Application.Tests/Infrastructure/Validators/UsernameValidatorTests.cs index 1e4571a..60daae4 100644 --- a/Govor.Application.Tests/Validators/UsernameValidatorTests.cs +++ b/Govor.Application.Tests/Infrastructure/Validators/UsernameValidatorTests.cs @@ -1,7 +1,7 @@ using Govor.Application.Exceptions.AuthService; using Govor.Application.Infrastructure.Validators; -namespace Govor.API.Tests.UnitTests.Services.Validators; +namespace Govor.Application.Tests.Infrastructure.Validators; [TestFixture] public class UsernameValidatorTests diff --git a/Govor.Application.Tests/Services/Medias/AccesserToDownloadMediaServiceTests.cs b/Govor.Application.Tests/Services/Medias/AccesserToDownloadMediaServiceTests.cs new file mode 100644 index 0000000..183d562 --- /dev/null +++ b/Govor.Application.Tests/Services/Medias/AccesserToDownloadMediaServiceTests.cs @@ -0,0 +1,152 @@ +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() + .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(); + + // Seed message from user to other user + 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 + }; + + 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 + }; + + 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); + } + + [TearDown] + public void TearDown() + { + _dbContext.Dispose(); + } +} diff --git a/Govor.Application.Tests/Services/UserSessions/UserSessionOpenerTests.cs b/Govor.Application.Tests/Services/UserSessions/UserSessionOpenerTests.cs index 490cf51..278c4f5 100644 --- a/Govor.Application.Tests/Services/UserSessions/UserSessionOpenerTests.cs +++ b/Govor.Application.Tests/Services/UserSessions/UserSessionOpenerTests.cs @@ -8,6 +8,8 @@ using Moq; using Govor.Application.Interfaces.Authentication; using Govor.Application.Services.Authentication; +namespace Govor.Application.Tests.Services.UserSessions; + [TestFixture] public class UserSessionOpenerTests { diff --git a/Govor.Application/Interfaces/Medias/IAccesserToDownloadMedia.cs b/Govor.Application/Interfaces/Medias/IAccesserToDownloadMedia.cs new file mode 100644 index 0000000..a55669e --- /dev/null +++ b/Govor.Application/Interfaces/Medias/IAccesserToDownloadMedia.cs @@ -0,0 +1,6 @@ +namespace Govor.Application.Interfaces.Medias; + +public interface IAccesserToDownloadMedia +{ + Task HasAccessAsync(Guid mediaFileId, Guid userId); +} \ No newline at end of file diff --git a/Govor.Application/Services/Medias/AccesserToDownloadMediaService.cs b/Govor.Application/Services/Medias/AccesserToDownloadMediaService.cs new file mode 100644 index 0000000..305ea53 --- /dev/null +++ b/Govor.Application/Services/Medias/AccesserToDownloadMediaService.cs @@ -0,0 +1,33 @@ +using Govor.Application.Interfaces.Medias; +using Govor.Core.Models.Messages; +using Govor.Data; +using Microsoft.EntityFrameworkCore; + +namespace Govor.Application.Services.Medias; + +public class AccesserToDownloadMediaService : IAccesserToDownloadMedia +{ + private readonly GovorDbContext _dbContext; + + public AccesserToDownloadMediaService(GovorDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task HasAccessAsync(Guid mediaFileId, Guid userId) + { + return await _dbContext.MediaAttachments + .Include(ma => ma.Message) + .AnyAsync(ma => + ma.MediaFileId == mediaFileId && + ( + (ma.Message.RecipientType == RecipientType.User && + (ma.Message.SenderId == userId || ma.Message.RecipientId == userId)) + || + (ma.Message.RecipientType == RecipientType.Group && + _dbContext.GroupMemberships.Any(gm => + gm.GroupId == ma.Message.RecipientId && + gm.UserId == userId)) + )); + } +} \ No newline at end of file diff --git a/Govor.Application/Services/Messages/MediaService.cs b/Govor.Application/Services/Medias/MediaService.cs similarity index 98% rename from Govor.Application/Services/Messages/MediaService.cs rename to Govor.Application/Services/Medias/MediaService.cs index 547e393..a8e9ff9 100644 --- a/Govor.Application/Services/Messages/MediaService.cs +++ b/Govor.Application/Services/Medias/MediaService.cs @@ -5,7 +5,7 @@ using Govor.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace Govor.Application.Services.Messages; +namespace Govor.Application.Services.Medias; public class MediaService : IMediaService { diff --git a/Govor.Data/Configurations/GroupMembershipConfiguration.cs b/Govor.Data/Configurations/GroupMembershipConfiguration.cs index 9cee30b..fb16926 100644 --- a/Govor.Data/Configurations/GroupMembershipConfiguration.cs +++ b/Govor.Data/Configurations/GroupMembershipConfiguration.cs @@ -12,7 +12,7 @@ public class GroupMembershipConfiguration : IEntityTypeConfiguration e.UserId).IsRequired(); builder.Property(e => e.GroupId).IsRequired(); - builder.Property(e => e.InvitationId).IsRequired(); + builder.Property(e => e.InvitationId).IsRequired(false); // Optional: можно добавить навигацию к GroupInvitation builder.HasOne()