diff --git a/services/identity/test/Board.IdentityService.Application.Tests/TokenServiceTests.cs b/services/identity/test/Board.IdentityService.Application.Tests/TokenServiceTests.cs new file mode 100644 index 0000000..4c884dd --- /dev/null +++ b/services/identity/test/Board.IdentityService.Application.Tests/TokenServiceTests.cs @@ -0,0 +1,287 @@ +using System.Text; +using System.Security.Claims; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Moq; +using Board.IdentityService.Application.Dtos; +using Board.IdentityService.Persistence.Domain; +using Board.IdentityService.Persistence.Infrastructure; +using Board.IdentityService.Application.Service.Interface; +using Board.IdentityService.Application.Service.Implementation; + +namespace Board.IdentityService.Application.Tests; + +public class TokenServiceTests : IDisposable +{ + private readonly AppDbContext _context; + private readonly ITokenService _tokenService; + private readonly string _jwtSecret = "test-secret-key-that-is-long-enough-for-hmacsha256"; + private readonly int _jwtExpiresMinutes = 30; + public TokenServiceTests() + { + _context = CreateInMemoryDbContext(); + _tokenService = new TokenService(_context); + } + + private AppDbContext CreateInMemoryDbContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new AppDbContext(options); + } + + public void Dispose() + { + _context.Database.EnsureDeleted(); + _context.Dispose(); + } + + [Fact] + public async Task GenerateTokenAsync_ValidUser_ReturnsTokenDto() + { + // Arrange + User user = new() + { + Id = "user-123", + Email = "test@example.com", + UserName = "testuser", + Roles = new List { "User" } + }; + + // Act + int initialCount = await _context.RefreshTokens.CountAsync(); + TokenDto result = await _tokenService.GenerateTokenAsync(user, _jwtSecret, _jwtExpiresMinutes); + int finalCount = await _context.RefreshTokens.CountAsync(); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Token); + Assert.NotNull(result.RefreshToken); + Assert.Equal(user.Id, result.User.Id); + Assert.Equal(user.Email, result.User.Email); + Assert.Equal(user.UserName, result.User.UserName); + Assert.True(result.ExpiresAt > DateTime.UtcNow); + Assert.Equal(initialCount + 1, finalCount); + } + + [Fact] + public async Task GenerateTokenAsync_RevokesExistingTokens() + { + // Arrange + string userId = "user-123"; + RefreshToken existingToken = new() + { + Id = "existing-token", + UserId = userId, + Token = "old-refresh-token", + IsRevoked = false, + Expires = DateTime.UtcNow.AddDays(1) + }; + User user = new() + { + Id = userId, + Email = "test@example.com", + UserName = "testuser" + }; + + // Act + await _context.RefreshTokens.AddAsync(existingToken); + await _context.SaveChangesAsync(); + await _tokenService.GenerateTokenAsync(user, _jwtSecret, _jwtExpiresMinutes); + RefreshToken updatedToken = await _context.RefreshTokens.FindAsync(existingToken.Id); + + // Assert + Assert.True(updatedToken.IsRevoked); + Assert.NotNull(updatedToken.Revoked); + } + + [Fact] + public async Task ValidateRefreshTokenAsync_ValidToken_ReturnsTrue() + { + // Arrange + string userId = "user-123"; + string refreshToken = "valid-refresh-token"; + RefreshToken storedToken = new() + { + Id = "token-1", + UserId = userId, + Token = refreshToken, + IsRevoked = false, + Expires = DateTime.UtcNow.AddDays(1) + }; + + // Act + await _context.RefreshTokens.AddAsync(storedToken); + await _context.SaveChangesAsync(); + bool result = await _tokenService.ValidateRefreshTokenAsync(userId, refreshToken); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task ValidateRefreshTokenAsync_ExpiredToken_ReturnsFalse() + { + // Arrange + string userId = "user-123"; + string refreshToken = "expired-token"; + RefreshToken storedToken = new() + { + Id = "token-1", + UserId = userId, + Token = refreshToken, + IsRevoked = false, + Expires = DateTime.UtcNow.AddDays(-1) + }; + + // Act + await _context.RefreshTokens.AddAsync(storedToken); + await _context.SaveChangesAsync(); + bool result = await _tokenService.ValidateRefreshTokenAsync(userId, refreshToken); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task ValidateRefreshTokenAsync_RevokedToken_ReturnsFalse() + { + // Arrange + string userId = "user-123"; + string refreshToken = "revoked-token"; + RefreshToken storedToken = new() + { + Id = "token-1", + UserId = userId, + Token = refreshToken, + IsRevoked = true, + Expires = DateTime.UtcNow.AddDays(1) + }; + + // Act + await _context.RefreshTokens.AddAsync(storedToken); + await _context.SaveChangesAsync(); + bool result = await _tokenService.ValidateRefreshTokenAsync(userId, refreshToken); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task ValidateToken_ValidToken_ReturnsValidateTokenDto() + { + // Arrange + User user = new() + { + Id = "user-123", + Email = "test@example.com", + Roles = new List { "User", "Admin" } + }; + + // Act + TokenDto token = await _tokenService.GenerateTokenAsync(user, _jwtSecret, _jwtExpiresMinutes); + ValidateTokenDto result = _tokenService.ValidateToken(token.Token, _jwtSecret); + + // Assert + Assert.NotNull(result); + Assert.True(result.IsValid); + Assert.Equal(user.Id, result.UserId); + Assert.Equal(user.Email, result.Email); + Assert.Equal(2, result.Roles.Count); + Assert.Contains("User", result.Roles); + Assert.Contains("Admin", result.Roles); + Assert.True(result.ExpiresAt > DateTime.UtcNow); + } + + [Fact] + public void ValidateToken_InvalidToken_ThrowsSecurityTokenException() + { + // Arrange + string invalidToken = "invalid.jwt.token"; + + // Act & Assert + Assert.ThrowsAny(() => _tokenService.ValidateToken(invalidToken, _jwtSecret)); + } + + [Fact] + public void GetPrincipalFromExpiredToken_ValidExpiredToken_ReturnsClaimsPrincipal() + { + // Arrange + User user = new() + { + Id = "user-123", + Email = "test@example.com", + Roles = new List { "User" } + }; + JwtSecurityTokenHandler tokenHandler = new(); + byte[] key = Encoding.ASCII.GetBytes(_jwtSecret); + List claims = new() + { + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim(ClaimTypes.Email, user.Email) + }; + SecurityTokenDescriptor tokenDescriptor = new() + { + Subject = new ClaimsIdentity(claims), + Expires = DateTime.UtcNow.AddMinutes(1), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }; + + // Act + SecurityToken expiredToken = tokenHandler.CreateToken(tokenDescriptor); + string tokenString = tokenHandler.WriteToken(expiredToken); + ClaimsPrincipal principal = _tokenService.GetPrincipalFromExpiredToken(tokenString, _jwtSecret); + string userIdClaim = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value; + string emailClaim = principal.FindFirst(ClaimTypes.Email)?.Value; + + // Assert + Assert.NotNull(principal); + Assert.Equal(user.Id, userIdClaim); + Assert.Equal(user.Email, emailClaim); + } + + [Fact] + public async Task RevokeRefreshTokensAsync_ValidUserId_RevokesAllTokens() + { + // Arrange + string userId = "user-123"; + List tokens = new() + { + new RefreshToken { Id = "1", UserId = userId, IsRevoked = false, Token = "" }, + new RefreshToken { Id = "2", UserId = userId, IsRevoked = false, Token = "" }, + new RefreshToken { Id = "3", UserId = "other-user", IsRevoked = false, Token = "" } + }; + + // Act + await _context.RefreshTokens.AddRangeAsync(tokens); + await _context.SaveChangesAsync(); + await _tokenService.RevokeRefreshTokensAsync(userId); + List updatedTokens = await _context.RefreshTokens.ToListAsync(); + List userTokens = updatedTokens.Where(t => t.UserId == userId).ToList(); + RefreshToken otherUserToken = updatedTokens.First(t => t.UserId == "other-user"); + + // Assert + Assert.All(userTokens, t => Assert.True(t.IsRevoked)); + Assert.All(userTokens, t => Assert.NotNull(t.Revoked)); + Assert.False(otherUserToken.IsRevoked); + Assert.Null(otherUserToken.Revoked); + } + + [Fact] + public void GenerateRefreshToken_ReturnsBase64String() + { + // Arrange + var method = typeof(TokenService).GetMethod("GenerateRefreshToken", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + // Act + string token = (string)method.Invoke(_tokenService, null); + + // Assert + Assert.NotNull(token); + Assert.NotEmpty(token); + Assert.Matches(@"^[A-Za-z0-9+/]+={0,2}$", token); + } +} \ No newline at end of file diff --git a/services/identity/test/Board.IdentityService.Application.Tests/UserServiceTests.cs b/services/identity/test/Board.IdentityService.Application.Tests/UserServiceTests.cs new file mode 100644 index 0000000..ceeae59 --- /dev/null +++ b/services/identity/test/Board.IdentityService.Application.Tests/UserServiceTests.cs @@ -0,0 +1,256 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; +using Moq; +using Microsoft.Extensions.Configuration; +using Board.IdentityService.Persistence.Domain; +using Board.IdentityService.Persistence.Infrastructure; +using Board.IdentityService.Application.Service.Interface; +using Board.IdentityService.Application.Service.Implementation; + +namespace Board.IdentityService.Application.Tests; + +public class UserServiceTests : IDisposable +{ + private readonly AppDbContext _context; + private readonly Mock _configurationMock; + private readonly Mock> _passwordHasherMock; + private readonly IUserService _service; + + public UserServiceTests() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + _context = new AppDbContext(options); + _configurationMock = new Mock(); + _passwordHasherMock = new Mock>(); + _service = new UserService(_configurationMock.Object, _context, _passwordHasherMock.Object); + } + + public void Dispose() + { + _context.Database.EnsureDeleted(); + _context.Dispose(); + } + + [Fact] + public async Task GetUserByUserNameAsync_UserExists_ReturnsUser() + { + // Arrange + User expectedUser = new() + { + Id = "1", + UserName = "testuser", + Email = "test@test.com", + PasswordHash = "hash", + CreatedUtcAt = DateTime.UtcNow + }; + + // Act + await _context.Users.AddAsync(expectedUser); + await _context.SaveChangesAsync(); + User result = await _service.GetUserByUserNameAsync("testuser"); + + // Assert + Assert.NotNull(result); + Assert.Equal("testuser", result.UserName); + Assert.Equal("test@test.com", result.Email); + } + + [Fact] + public async Task GetUserByUserNameAsync_UserNotExists_ReturnsNull() + { + // Arrange + string username = "nonexistent"; + + // Act + User result = await _service.GetUserByUserNameAsync(username); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task GetUserByIdAsync_UserExists_ReturnsUser() + { + // Arrange + User expectedUser = new() + { + Id = "user-123", + UserName = "testuser", + Email = "test@test.com", + PasswordHash = "hash", + CreatedUtcAt = DateTime.UtcNow + }; + await _context.Users.AddAsync(expectedUser); + await _context.SaveChangesAsync(); + + // Act + User result = await _service.GetUserByIdAsync("user-123"); + + // Assert + Assert.NotNull(result); + Assert.Equal("user-123", result.Id); + Assert.Equal("testuser", result.UserName); + } + + [Fact] + public async Task CreateUserAsync_ValidData_CreatesUserWithRole() + { + // Arrange + _passwordHasherMock + .Setup(x => x.HashPassword(It.IsAny(), "password123")) + .Returns("hashed_password"); + + // Act + User result = await _service.CreateUserAsync("newuser", "password123", "newuser@test.com"); + User? userInDb = await _context.Users.FirstOrDefaultAsync(x => x.UserName == "newuser"); + + // Assert + Assert.NotNull(result); + Assert.Equal("newuser", result.UserName); + Assert.Equal("newuser@test.com", result.Email); + Assert.Equal("hashed_password", result.PasswordHash); + Assert.NotEmpty(result.Id); + Assert.True(result.CreatedUtcAt <= DateTime.UtcNow.AddSeconds(10)); + Assert.NotNull(userInDb); + } + + [Fact] + public async Task CreateUserAsync_UserAlreadyExists_ThrowsException() + { + // Arrange + User existingUser = new() + { + Id = "1", + UserName = "existinguser", + Email = "existing@test.com", + PasswordHash = "hash", + CreatedUtcAt = DateTime.UtcNow + }; + + await _context.Users.AddAsync(existingUser); + await _context.SaveChangesAsync(); + + // Act & Assert + await Assert.ThrowsAsync(() => + _service.CreateUserAsync("existinguser", "password123", "newemail@test.com")); + } + + [Fact] + public async Task CreateUserAsync_RoleNotFound_CreatesUserWithoutRole() + { + // Arrange + _passwordHasherMock + .Setup(x => x.HashPassword(It.IsAny(), "password123")) + .Returns("hashed_password"); + + // Act + User result = await _service.CreateUserAsync("newuser", "password123", "newuser@test.com"); + + // Assert + Assert.NotNull(result); + Assert.Equal("newuser", result.UserName); + Assert.True(result.Roles.Count == 0); + } + + [Fact] + public async Task AuthenticateAsync_ValidCredentials_ReturnsUser() + { + // Arrange + User testUser = new() + { + Id = "1", + UserName = "testuser", + Email = "test@test.com", + PasswordHash = "hashed_password", + CreatedUtcAt = DateTime.UtcNow + }; + + // Act + await _context.Users.AddAsync(testUser); + await _context.SaveChangesAsync(); + _passwordHasherMock + .Setup(x => x.VerifyHashedPassword(testUser, "hashed_password", "correctpassword")) + .Returns(PasswordVerificationResult.Success); + User result = await _service.AuthenticateAsync("testuser", "correctpassword"); + + // Assert + Assert.NotNull(result); + Assert.Equal("testuser", result.UserName); + } + + [Fact] + public async Task AuthenticateAsync_UserNotExists_ReturnsNull() + { + // Arrange + string username = "nonexistent"; + string password = "password"; + + // Act + User result = await _service.AuthenticateAsync(username, password); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task AuthenticateAsync_WrongPassword_ReturnsNull() + { + // Arrange + User testUser = new() + { + Id = "1", + UserName = "testuser", + Email = "test@test.com", + PasswordHash = "hashed_password", + CreatedUtcAt = DateTime.UtcNow + }; + + await _context.Users.AddAsync(testUser); + await _context.SaveChangesAsync(); + _passwordHasherMock + .Setup(x => x.VerifyHashedPassword(testUser, "hashed_password", "wrongpassword")) + .Returns(PasswordVerificationResult.Failed); + + // Act + User result = await _service.AuthenticateAsync("testuser", "wrongpassword"); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task CreateUserAsync_PasswordIsHashed() + { + // Arrange + string expectedHash = "expected_hashed_password"; + _passwordHasherMock + .Setup(x => x.HashPassword(It.IsAny(), "TestPassword123")) + .Returns(expectedHash); + + // Act + User result = await _service.CreateUserAsync("testuser", "TestPassword123", "test@test.com"); + _passwordHasherMock.Verify(x => x.HashPassword(It.IsAny(), "TestPassword123"), Times.Once); + + // Assert + Assert.Equal(expectedHash, result.PasswordHash); + } + + [Fact] + public async Task CreateUserAsync_MultipleUsers_CreatesWithUniqueIds() + { + // Arrange + _passwordHasherMock + .Setup(x => x.HashPassword(It.IsAny(), It.IsAny())) + .Returns("hashed_password"); + + // Act + User user1 = await _service.CreateUserAsync("user1", "password1", "user1@test.com"); + User user2 = await _service.CreateUserAsync("user2", "password2", "user2@test.com"); + + // Assert + Assert.NotEqual(user1.Id, user2.Id); + Assert.Equal(2, await _context.Users.CountAsync()); + } +} \ No newline at end of file diff --git a/services/message/src/Board.MessageService.Application/Dtos/GroupResultDto.cs b/services/message/src/Board.MessageService.Application/Dtos/GroupResultDto.cs index bb9db09..ccbeeb3 100644 --- a/services/message/src/Board.MessageService.Application/Dtos/GroupResultDto.cs +++ b/services/message/src/Board.MessageService.Application/Dtos/GroupResultDto.cs @@ -5,5 +5,5 @@ public class GroupResultDto public string Name { get; set; } public string Title { get; set; } public string Description { get; set; } - public List Messages { get; set; } = new(); + public IEnumerable Messages { get; set; } = Enumerable.Empty(); } \ No newline at end of file diff --git a/services/message/src/Board.MessageService.Application/Dtos/MessageDto.cs b/services/message/src/Board.MessageService.Application/Dtos/MessageDto.cs index f73006b..8f1d241 100644 --- a/services/message/src/Board.MessageService.Application/Dtos/MessageDto.cs +++ b/services/message/src/Board.MessageService.Application/Dtos/MessageDto.cs @@ -7,5 +7,6 @@ public class MessageDto public string UserName { get; set; } public string UserNameColor { get; set; } public string? UserAvatarUrl { get; set; } - public List Attachments { get; set; } = new List(); + public DateTime CreatedUtcAt { get; set; } + public IEnumerable Attachments { get; set; } = Enumerable.Empty(); } \ No newline at end of file diff --git a/services/message/src/Board.MessageService.Application/Service/Implementation/MessageService.cs b/services/message/src/Board.MessageService.Application/Service/Implementation/MessageService.cs index c5b0190..fd8cfdb 100644 --- a/services/message/src/Board.MessageService.Application/Service/Implementation/MessageService.cs +++ b/services/message/src/Board.MessageService.Application/Service/Implementation/MessageService.cs @@ -75,28 +75,30 @@ public async Task GetPageAsync(string groupName, int page) { throw new KeyNotFoundException("Group/page not found"); } - List messages = await _context.Messages + List messages = await _context.Messages .AsNoTracking() .Where(x => x.GroupId == group.Id) - .OrderBy(x => x.Id) + .OrderBy(x => x.CreatedUtcAt) .Include(x => x.Attachments) .Skip(page * 100) .Take(100) + .Select(x => new MessageDto() + { + Id = x.Id, + Text = x.Text, + UserAvatarUrl = x.UserAvatarUrl, + UserName = x.UserName, + UserNameColor = x.UserNameColor, + CreatedUtcAt = x.CreatedUtcAt, + Attachments = x.Attachments.Select(a => a.Url) + }) .ToListAsync(); GroupResultDto groupResultDto = new() { Name = groupName, Title = group.Title, Description = group.Description, - Messages = messages.Select(message => new MessageDto() - { - Id = message.Id, - Text = message.Text, - UserAvatarUrl = message.UserAvatarUrl, - UserName = message.UserName, - UserNameColor = message.UserNameColor, - Attachments = message.Attachments.Select(x => x.Url).ToList() - }).ToList() + Messages = messages }; return groupResultDto; } diff --git a/services/message/src/Board.MessageService.Persistence/Infrastructure/AppDbContext.cs b/services/message/src/Board.MessageService.Persistence/Infrastructure/AppDbContext.cs index c533cb6..3caed64 100644 --- a/services/message/src/Board.MessageService.Persistence/Infrastructure/AppDbContext.cs +++ b/services/message/src/Board.MessageService.Persistence/Infrastructure/AppDbContext.cs @@ -34,6 +34,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(m => m.UserNameColor) .HasMaxLength(10); + + entity.HasIndex(m => new { m.GroupId, m.CreatedUtcAt }); }); modelBuilder.Entity(entity => diff --git a/services/message/src/Board.MessageService.Persistence/Migrations/20260214144305_AddDatetimeIndex.Designer.cs b/services/message/src/Board.MessageService.Persistence/Migrations/20260214144305_AddDatetimeIndex.Designer.cs new file mode 100644 index 0000000..5a4190b --- /dev/null +++ b/services/message/src/Board.MessageService.Persistence/Migrations/20260214144305_AddDatetimeIndex.Designer.cs @@ -0,0 +1,156 @@ +// +using System; +using Board.MessageService.Persistence.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Board.MessageService.Persistence.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260214144305_AddDatetimeIndex")] + partial class AddDatetimeIndex + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Board.MessageService.Persistence.Domain.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AvatarUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("IX_Group_Name"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Board.MessageService.Persistence.Domain.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedUtcAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserAvatarUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserNameColor") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.HasKey("Id"); + + b.HasIndex("GroupId", "CreatedUtcAt"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("Board.MessageService.Persistence.Domain.MessageAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("OrderIndex") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.ToTable("MessagesAttachments"); + }); + + modelBuilder.Entity("Board.MessageService.Persistence.Domain.Message", b => + { + b.HasOne("Board.MessageService.Persistence.Domain.Group", "Group") + .WithMany("Messages") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Board.MessageService.Persistence.Domain.MessageAttachment", b => + { + b.HasOne("Board.MessageService.Persistence.Domain.Message", "Message") + .WithMany("Attachments") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("Board.MessageService.Persistence.Domain.Group", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Board.MessageService.Persistence.Domain.Message", b => + { + b.Navigation("Attachments"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/services/message/src/Board.MessageService.Persistence/Migrations/20260214144305_AddDatetimeIndex.cs b/services/message/src/Board.MessageService.Persistence/Migrations/20260214144305_AddDatetimeIndex.cs new file mode 100644 index 0000000..2e8d735 --- /dev/null +++ b/services/message/src/Board.MessageService.Persistence/Migrations/20260214144305_AddDatetimeIndex.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Board.MessageService.Persistence.Migrations +{ + /// + public partial class AddDatetimeIndex : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Messages_GroupId", + table: "Messages"); + + migrationBuilder.CreateIndex( + name: "IX_Messages_GroupId_CreatedUtcAt", + table: "Messages", + columns: new[] { "GroupId", "CreatedUtcAt" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Messages_GroupId_CreatedUtcAt", + table: "Messages"); + + migrationBuilder.CreateIndex( + name: "IX_Messages_GroupId", + table: "Messages", + column: "GroupId"); + } + } +} diff --git a/services/message/src/Board.MessageService.Persistence/Migrations/AppDbContextModelSnapshot.cs b/services/message/src/Board.MessageService.Persistence/Migrations/AppDbContextModelSnapshot.cs index a843ba4..f66c053 100644 --- a/services/message/src/Board.MessageService.Persistence/Migrations/AppDbContextModelSnapshot.cs +++ b/services/message/src/Board.MessageService.Persistence/Migrations/AppDbContextModelSnapshot.cs @@ -88,7 +88,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("GroupId"); + b.HasIndex("GroupId", "CreatedUtcAt"); b.ToTable("Messages"); });