1204 lines
49 KiB
C#
1204 lines
49 KiB
C#
using Ikar.Server.Controllers;
|
|
using Ikar.Server.Data.Entities;
|
|
using Ikar.Shared;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Ikar.Server.Tests;
|
|
|
|
public sealed class ChannelMembersControllerTests
|
|
{
|
|
[Fact]
|
|
public async Task GetPublicChannelByUsername_ReturnsPreviewForNonMember()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var viewer = CreateUser("viewer");
|
|
var channel = CreateChannel(owner);
|
|
channel.PublicUsername = "newsroom";
|
|
dbContext.AddRange(owner, viewer, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, viewer.Id);
|
|
var response = await controller.GetPublicChannelByUsername("@newsroom", CancellationToken.None);
|
|
|
|
var details = ControllerTestFixture.ValueOf(response);
|
|
Assert.Equal(channel.Id, details.Id);
|
|
Assert.Equal(ChatType.Channel, details.Type);
|
|
Assert.False(details.IsSubscribed);
|
|
Assert.True(details.CanSubscribe);
|
|
Assert.Equal("newsroom", details.PublicUsername);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchPage_ReturnsPublicChannelForNonMember()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var viewer = CreateUser("viewer");
|
|
var channel = CreateChannel(owner);
|
|
channel.Title = "City News";
|
|
channel.PublicUsername = "city_news";
|
|
channel.Description = "Daily city updates";
|
|
dbContext.AddRange(owner, viewer, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, viewer.Id);
|
|
var response = await controller.SearchPage(
|
|
q: "city",
|
|
filter: null,
|
|
type: null,
|
|
page: 1,
|
|
limit: 20,
|
|
date: null,
|
|
dateFrom: null,
|
|
dateTo: null,
|
|
authorId: null,
|
|
author: null,
|
|
CancellationToken.None);
|
|
|
|
var page = ControllerTestFixture.ValueOf(response);
|
|
var result = Assert.Single(page.Results, x => x.Chat.Id == channel.Id);
|
|
Assert.Null(result.Message);
|
|
Assert.Equal(ChatType.Channel, result.Chat.Type);
|
|
Assert.Equal("city_news", result.Chat.PublicUsername);
|
|
Assert.True(result.Chat.CanSubscribe);
|
|
Assert.False(result.Chat.IsSubscribed);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddMembers_CanAddBotAsChannelAdmin()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var bot = CreateUser("news_bot", isBot: true);
|
|
var channel = CreateChannel(owner);
|
|
dbContext.AddRange(owner, bot, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.AddMembers(
|
|
channel.Id,
|
|
new AddChatMembersRequest([bot.Id], ChatMemberRole.Admin),
|
|
CancellationToken.None);
|
|
|
|
var details = ControllerTestFixture.ValueOf(response);
|
|
var members = details.Members ?? throw new InvalidOperationException("Chat members were not returned.");
|
|
var botMember = Assert.Single(members, member => member.User.Id == bot.Id);
|
|
Assert.True(botMember.User.IsBot);
|
|
Assert.Equal(ChatMemberRole.Admin, botMember.Role);
|
|
Assert.True(botMember.Permissions.CanManageMembers);
|
|
Assert.True(botMember.Permissions.CanPostMessages);
|
|
Assert.True(botMember.Permissions.CanPinMessages);
|
|
Assert.True(botMember.Permissions.CanDeleteMessages);
|
|
Assert.True(botMember.Permissions.CanInviteUsers);
|
|
Assert.Equal(2, details.SubscriberCount);
|
|
Assert.Equal(2, details.AdminCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddMembers_CanAddMemberToGroup()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var member = CreateUser("member");
|
|
var group = CreateGroup(owner);
|
|
dbContext.AddRange(owner, member, group);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.AddMembers(
|
|
group.Id,
|
|
new AddChatMembersRequest([member.Id]),
|
|
CancellationToken.None);
|
|
|
|
var details = ControllerTestFixture.ValueOf(response);
|
|
var addedMember = Assert.Single(details.Members ?? [], x => x.User.Id == member.Id);
|
|
Assert.Equal(ChatMemberRole.Member, addedMember.Role);
|
|
Assert.False(addedMember.IsOwner);
|
|
Assert.True(addedMember.Permissions.CanPostMessages);
|
|
Assert.Equal(2, details.SubscriberCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddMembers_DeniesWhenCandidateBlockedAdmin()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var member = CreateUser("member");
|
|
var group = CreateGroup(owner);
|
|
dbContext.AddRange(
|
|
owner,
|
|
member,
|
|
group,
|
|
new UserPrivacySettings
|
|
{
|
|
UserId = member.Id,
|
|
User = member,
|
|
BlockedUserIds = owner.Id.ToString()
|
|
});
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.AddMembers(
|
|
group.Id,
|
|
new AddChatMembersRequest([member.Id]),
|
|
CancellationToken.None);
|
|
|
|
var forbidden = Assert.IsType<Microsoft.AspNetCore.Mvc.ObjectResult>(response.Result);
|
|
Assert.Equal(StatusCodes.Status403Forbidden, forbidden.StatusCode);
|
|
Assert.DoesNotContain(dbContext.ChatMembers, x => x.ChatId == group.Id && x.UserId == member.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendMessage_DeniesRestrictedGroupMember()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var member = CreateUser("member");
|
|
var group = CreateGroup(owner);
|
|
group.Members.Add(new ChatMember
|
|
{
|
|
ChatId = group.Id,
|
|
Chat = group,
|
|
UserId = member.Id,
|
|
User = member,
|
|
Role = ChatMemberRole.Member,
|
|
CanPostMessages = false
|
|
});
|
|
dbContext.AddRange(owner, member, group);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, member.Id);
|
|
var response = await controller.SendMessage(
|
|
group.Id,
|
|
new SendMessageRequest("test"),
|
|
CancellationToken.None);
|
|
|
|
var forbidden = Assert.IsType<Microsoft.AspNetCore.Mvc.ObjectResult>(response.Result);
|
|
Assert.Equal(StatusCodes.Status403Forbidden, forbidden.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RemoveMember_CanRemoveRegularChannelMember()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var member = CreateUser("member");
|
|
var channel = CreateChannel(owner);
|
|
channel.Members.Add(new ChatMember
|
|
{
|
|
ChatId = channel.Id,
|
|
Chat = channel,
|
|
UserId = member.Id,
|
|
User = member,
|
|
Role = ChatMemberRole.Member
|
|
});
|
|
dbContext.AddRange(owner, member, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.RemoveMember(channel.Id, member.Id, CancellationToken.None);
|
|
|
|
var details = ControllerTestFixture.ValueOf(response);
|
|
Assert.DoesNotContain(details.Members ?? [], x => x.User.Id == member.Id);
|
|
Assert.False(dbContext.ChatMembers.Any(x => x.ChatId == channel.Id && x.UserId == member.Id));
|
|
Assert.Equal(1, details.SubscriberCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task JoinByInviteLink_ReturnsPendingApprovalResultForModeratedInvite()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var requester = CreateUser("requester");
|
|
var channel = CreateChannel(owner);
|
|
var invite = new ChatInviteLink
|
|
{
|
|
ChatId = channel.Id,
|
|
Chat = channel,
|
|
CreatedById = owner.Id,
|
|
CreatedBy = owner,
|
|
Token = "moderated-token",
|
|
CreatesJoinRequest = true
|
|
};
|
|
channel.InviteLinks.Add(invite);
|
|
dbContext.AddRange(owner, requester, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, requester.Id);
|
|
var response = await controller.JoinByInviteLink(invite.Token, CancellationToken.None);
|
|
|
|
var accepted = Assert.IsType<Microsoft.AspNetCore.Mvc.AcceptedResult>(response.Result);
|
|
var result = Assert.IsType<JoinChatResultDto>(accepted.Value);
|
|
Assert.Equal(JoinChatResultStatus.PendingApproval, result.Status);
|
|
Assert.NotNull(result.JoinRequestId);
|
|
Assert.Equal(ChatJoinRequestStatus.Pending, result.JoinRequestStatus);
|
|
Assert.Null(result.Chat);
|
|
Assert.Single(dbContext.ChatJoinRequests, x => x.ChatId == channel.Id && x.UserId == requester.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateInviteLink_ReturnsExpirationAndUsageLimits()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var channel = CreateChannel(owner);
|
|
dbContext.AddRange(owner, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
var expiresAt = DateTimeOffset.UtcNow.AddHours(2);
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.CreateInviteLink(
|
|
channel.Id,
|
|
new CreateChatInviteLinkRequest(
|
|
Title: "limited",
|
|
CreatesJoinRequest: false,
|
|
ExpiresAt: expiresAt,
|
|
MaxUses: 3,
|
|
MemberLimit: 10),
|
|
CancellationToken.None);
|
|
|
|
var link = ControllerTestFixture.ValueOf(response);
|
|
Assert.Equal("limited", link.Title);
|
|
Assert.False(link.CreatesJoinRequest);
|
|
Assert.Equal(expiresAt, link.ExpiresAt);
|
|
Assert.Equal(3, link.MaxUses);
|
|
Assert.Equal(0, link.UsesCount);
|
|
Assert.Equal(10, link.MemberLimit);
|
|
|
|
var stored = Assert.Single(dbContext.ChatInviteLinks, x => x.ChatId == channel.Id);
|
|
Assert.Equal(expiresAt, stored.ExpiresAt);
|
|
Assert.Equal(3, stored.MaxUses);
|
|
Assert.Equal(0, stored.UsesCount);
|
|
Assert.Equal(10, stored.MemberLimit);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task JoinByInviteLink_RejectsExpiredLink()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var viewer = CreateUser("viewer");
|
|
var channel = CreateChannel(owner);
|
|
var invite = new ChatInviteLink
|
|
{
|
|
ChatId = channel.Id,
|
|
Chat = channel,
|
|
CreatedById = owner.Id,
|
|
CreatedBy = owner,
|
|
Token = "expired-token",
|
|
ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(-1)
|
|
};
|
|
channel.InviteLinks.Add(invite);
|
|
dbContext.AddRange(owner, viewer, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, viewer.Id);
|
|
var response = await controller.JoinByInviteLink(invite.Token, CancellationToken.None);
|
|
|
|
var badRequest = Assert.IsType<Microsoft.AspNetCore.Mvc.BadRequestObjectResult>(response.Result);
|
|
Assert.Equal("Invite link has expired.", badRequest.Value);
|
|
Assert.Equal(0, invite.UsesCount);
|
|
Assert.DoesNotContain(dbContext.ChatMembers, x => x.ChatId == channel.Id && x.UserId == viewer.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task JoinByInviteLink_IncrementsUsesAndRejectsAfterMaxUses()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var firstViewer = CreateUser("first");
|
|
var secondViewer = CreateUser("second");
|
|
var channel = CreateChannel(owner);
|
|
var invite = new ChatInviteLink
|
|
{
|
|
ChatId = channel.Id,
|
|
Chat = channel,
|
|
CreatedById = owner.Id,
|
|
CreatedBy = owner,
|
|
Token = "single-use-token",
|
|
MaxUses = 1
|
|
};
|
|
channel.InviteLinks.Add(invite);
|
|
dbContext.AddRange(owner, firstViewer, secondViewer, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var firstController = ControllerTestFixture.CreateChatsController(dbContext, firstViewer.Id);
|
|
var firstResponse = await firstController.JoinByInviteLink(invite.Token, CancellationToken.None);
|
|
|
|
var firstResult = ControllerTestFixture.ValueOf(firstResponse);
|
|
Assert.Equal(JoinChatResultStatus.Joined, firstResult.Status);
|
|
Assert.Equal(1, invite.UsesCount);
|
|
Assert.Contains(dbContext.ChatMembers, x => x.ChatId == channel.Id && x.UserId == firstViewer.Id);
|
|
|
|
var secondController = ControllerTestFixture.CreateChatsController(dbContext, secondViewer.Id);
|
|
var secondResponse = await secondController.JoinByInviteLink(invite.Token, CancellationToken.None);
|
|
|
|
var badRequest = Assert.IsType<Microsoft.AspNetCore.Mvc.BadRequestObjectResult>(secondResponse.Result);
|
|
Assert.Equal("Invite link usage limit reached.", badRequest.Value);
|
|
Assert.Equal(1, invite.UsesCount);
|
|
Assert.DoesNotContain(dbContext.ChatMembers, x => x.ChatId == channel.Id && x.UserId == secondViewer.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task JoinByInviteLink_RejectsWhenMemberLimitReached()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var viewer = CreateUser("viewer");
|
|
var channel = CreateChannel(owner);
|
|
var invite = new ChatInviteLink
|
|
{
|
|
ChatId = channel.Id,
|
|
Chat = channel,
|
|
CreatedById = owner.Id,
|
|
CreatedBy = owner,
|
|
Token = "full-token",
|
|
MemberLimit = 1
|
|
};
|
|
channel.InviteLinks.Add(invite);
|
|
dbContext.AddRange(owner, viewer, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, viewer.Id);
|
|
var response = await controller.JoinByInviteLink(invite.Token, CancellationToken.None);
|
|
|
|
var badRequest = Assert.IsType<Microsoft.AspNetCore.Mvc.BadRequestObjectResult>(response.Result);
|
|
Assert.Equal("Chat member limit reached.", badRequest.Value);
|
|
Assert.Equal(0, invite.UsesCount);
|
|
Assert.DoesNotContain(dbContext.ChatMembers, x => x.ChatId == channel.Id && x.UserId == viewer.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateMemberPermissions_CannotPromoteMemberToOwner()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var member = CreateUser("member");
|
|
var channel = CreateChannel(owner);
|
|
channel.Members.Add(new ChatMember
|
|
{
|
|
ChatId = channel.Id,
|
|
Chat = channel,
|
|
UserId = member.Id,
|
|
User = member,
|
|
IsOwner = false,
|
|
Role = ChatMemberRole.Member
|
|
});
|
|
dbContext.AddRange(owner, member, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.UpdateMemberPermissions(
|
|
channel.Id,
|
|
member.Id,
|
|
new UpdateChatMemberPermissionsRequest(
|
|
ChatMemberRole.Owner,
|
|
CanPinMessages: true,
|
|
CanDeleteMessages: true,
|
|
CanInviteUsers: true,
|
|
CanPostMessages: true),
|
|
CancellationToken.None);
|
|
|
|
var badRequest = Assert.IsType<Microsoft.AspNetCore.Mvc.BadRequestObjectResult>(response.Result);
|
|
Assert.Equal("Ownership can only be transferred by a dedicated owner transfer action.", badRequest.Value);
|
|
|
|
var members = dbContext.ChatMembers.Where(x => x.ChatId == channel.Id).ToList();
|
|
Assert.Single(members, x => x.IsOwner || x.Role == ChatMemberRole.Owner);
|
|
Assert.Equal(ChatMemberRole.Member, Assert.Single(members, x => x.UserId == member.Id).Role);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateChatState_CanLinkAndClearChannelDiscussionGroup()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var channel = CreateChannel(owner);
|
|
var group = CreateGroup(owner);
|
|
dbContext.AddRange(owner, channel, group);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
|
|
var linkResponse = await controller.UpdateChatState(
|
|
channel.Id,
|
|
new ChatStateRequest(LinkedDiscussionChatId: group.Id),
|
|
CancellationToken.None);
|
|
|
|
var linked = ControllerTestFixture.ValueOf(linkResponse);
|
|
Assert.Equal(group.Id, linked.LinkedDiscussionChatId);
|
|
Assert.Equal(group.Id, dbContext.Chats.Single(x => x.Id == channel.Id).LinkedDiscussionChatId);
|
|
|
|
var clearResponse = await controller.UpdateChatState(
|
|
channel.Id,
|
|
new ChatStateRequest(ClearLinkedDiscussionChat: true),
|
|
CancellationToken.None);
|
|
|
|
var cleared = ControllerTestFixture.ValueOf(clearResponse);
|
|
Assert.Null(cleared.LinkedDiscussionChatId);
|
|
Assert.Null(dbContext.Chats.Single(x => x.Id == channel.Id).LinkedDiscussionChatId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SendMessage_ChannelWithLinkedDiscussion_CreatesDiscussionRootAndCountsNestedReplies()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var channel = CreateChannel(owner);
|
|
var group = CreateGroup(owner);
|
|
channel.LinkedDiscussionChatId = group.Id;
|
|
dbContext.AddRange(owner, channel, group);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var postResponse = await controller.SendMessage(
|
|
channel.Id,
|
|
new SendMessageRequest("Breaking news"),
|
|
CancellationToken.None);
|
|
|
|
var postDto = ControllerTestFixture.ValueOf(postResponse);
|
|
Assert.NotNull(postDto.DiscussionMessageId);
|
|
Assert.Equal(0, postDto.CommentCount);
|
|
|
|
var rootMessage = dbContext.Messages.Single(x => x.Id == postDto.DiscussionMessageId);
|
|
Assert.Equal(group.Id, rootMessage.ChatId);
|
|
Assert.Equal("Breaking news", rootMessage.Text);
|
|
Assert.Equal(channel.Title, rootMessage.ForwardedFromDisplayName);
|
|
|
|
var firstReplyResponse = await controller.SendMessage(
|
|
group.Id,
|
|
new SendMessageRequest("First comment", ReplyToMessageId: rootMessage.Id),
|
|
CancellationToken.None);
|
|
var firstReply = ControllerTestFixture.ValueOf(firstReplyResponse);
|
|
|
|
await controller.SendMessage(
|
|
group.Id,
|
|
new SendMessageRequest("Nested comment", ReplyToMessageId: firstReply.Id),
|
|
CancellationToken.None);
|
|
|
|
var storedPost = dbContext.Messages.Single(x => x.Id == postDto.Id);
|
|
Assert.Equal(2, storedPost.CommentCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UploadAttachment_ChannelWithLinkedDiscussion_MirrorsAttachmentToDiscussionRoot()
|
|
{
|
|
var tempRoot = Path.Combine(Path.GetTempPath(), $"ikar-channel-upload-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempRoot);
|
|
try
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var channel = CreateChannel(owner);
|
|
var group = CreateGroup(owner);
|
|
channel.LinkedDiscussionChatId = group.Id;
|
|
dbContext.AddRange(owner, channel, group);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var storage = ControllerTestFixture.CreateAttachmentStorageService(tempRoot);
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id, storage);
|
|
var response = await controller.UploadAttachment(
|
|
channel.Id,
|
|
new UploadAttachmentRequest
|
|
{
|
|
Text = "Photo post",
|
|
File = CreateFormFile([0xFF, 0xD8, 0xFF, 0xE0], "photo.jpg", "image/jpeg")
|
|
},
|
|
CancellationToken.None);
|
|
|
|
var postDto = ControllerTestFixture.ValueOf(response);
|
|
Assert.NotNull(postDto.DiscussionMessageId);
|
|
|
|
var discussionRoot = dbContext.Messages
|
|
.Include(x => x.Attachments)
|
|
.Single(x => x.Id == postDto.DiscussionMessageId);
|
|
var mirroredAttachment = Assert.Single(discussionRoot.Attachments);
|
|
Assert.Equal(group.Id, discussionRoot.ChatId);
|
|
Assert.Equal("Photo post", discussionRoot.Text);
|
|
Assert.Equal("photo.jpg", mirroredAttachment.OriginalFileName);
|
|
Assert.Equal("image/jpeg", mirroredAttachment.ContentType);
|
|
Assert.True(File.Exists(mirroredAttachment.StoragePath));
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(tempRoot))
|
|
{
|
|
Directory.Delete(tempRoot, recursive: true);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UploadAttachmentAlbum_ChannelWithLinkedDiscussion_MirrorsAlbumToDiscussionRoot()
|
|
{
|
|
var tempRoot = Path.Combine(Path.GetTempPath(), $"ikar-channel-album-{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempRoot);
|
|
try
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var channel = CreateChannel(owner);
|
|
var group = CreateGroup(owner);
|
|
channel.LinkedDiscussionChatId = group.Id;
|
|
dbContext.AddRange(owner, channel, group);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var storage = ControllerTestFixture.CreateAttachmentStorageService(tempRoot);
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id, storage);
|
|
var response = await controller.UploadAttachmentAlbum(
|
|
channel.Id,
|
|
new UploadAttachmentAlbumRequest
|
|
{
|
|
Text = "Gallery",
|
|
Files =
|
|
[
|
|
CreateFormFile([0xFF, 0xD8, 0xFF, 0xE0], "first.jpg", "image/jpeg"),
|
|
CreateFormFile([0xFF, 0xD8, 0xFF, 0xE1], "second.jpg", "image/jpeg")
|
|
]
|
|
},
|
|
CancellationToken.None);
|
|
|
|
var postDto = ControllerTestFixture.ValueOf(response);
|
|
Assert.NotNull(postDto.DiscussionMessageId);
|
|
|
|
var channelPost = dbContext.Messages.Single(x => x.Id == postDto.Id);
|
|
var discussionRoot = dbContext.Messages
|
|
.Include(x => x.Attachments)
|
|
.Single(x => x.Id == postDto.DiscussionMessageId);
|
|
Assert.Equal(channelPost.MediaAlbumId, discussionRoot.MediaAlbumId);
|
|
Assert.Equal(
|
|
new[] { "first.jpg", "second.jpg" },
|
|
discussionRoot.Attachments.OrderBy(x => x.SortOrder).Select(x => x.OriginalFileName));
|
|
Assert.All(discussionRoot.Attachments, attachment => Assert.True(File.Exists(attachment.StoragePath)));
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(tempRoot))
|
|
{
|
|
Directory.Delete(tempRoot, recursive: true);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateMessage_ChannelPostWithLinkedDiscussion_UpdatesDiscussionRootText()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var channel = CreateChannel(owner);
|
|
var group = CreateGroup(owner);
|
|
channel.LinkedDiscussionChatId = group.Id;
|
|
dbContext.AddRange(owner, channel, group);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var postResponse = await controller.SendMessage(
|
|
channel.Id,
|
|
new SendMessageRequest("Old text"),
|
|
CancellationToken.None);
|
|
var postDto = ControllerTestFixture.ValueOf(postResponse);
|
|
|
|
var updateResponse = await controller.UpdateMessage(
|
|
channel.Id,
|
|
postDto.Id,
|
|
new UpdateMessageRequest("New text"),
|
|
CancellationToken.None);
|
|
|
|
var updatedDto = ControllerTestFixture.ValueOf(updateResponse);
|
|
var discussionRoot = dbContext.Messages.Single(x => x.Id == postDto.DiscussionMessageId);
|
|
Assert.Equal("New text", updatedDto.Text);
|
|
Assert.Equal("New text", discussionRoot.Text);
|
|
Assert.NotNull(discussionRoot.EditedAt);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteMessage_ChannelPostWithLinkedDiscussion_DeletesDiscussionRoot()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var channel = CreateChannel(owner);
|
|
var group = CreateGroup(owner);
|
|
channel.LinkedDiscussionChatId = group.Id;
|
|
dbContext.AddRange(owner, channel, group);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var postResponse = await controller.SendMessage(
|
|
channel.Id,
|
|
new SendMessageRequest("Post to delete"),
|
|
CancellationToken.None);
|
|
var postDto = ControllerTestFixture.ValueOf(postResponse);
|
|
|
|
var deleteResponse = await controller.DeleteMessage(channel.Id, postDto.Id, CancellationToken.None);
|
|
|
|
var deletedDto = ControllerTestFixture.ValueOf(deleteResponse);
|
|
var discussionRoot = dbContext.Messages.Single(x => x.Id == postDto.DiscussionMessageId);
|
|
Assert.NotNull(deletedDto.DeletedAt);
|
|
Assert.NotNull(discussionRoot.DeletedAt);
|
|
Assert.Equal(string.Empty, discussionRoot.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateChatState_CanUpdateChannelProfile()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var channel = CreateChannel(owner);
|
|
dbContext.AddRange(owner, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.UpdateChatState(
|
|
channel.Id,
|
|
new ChatStateRequest(
|
|
Title: "Daily News",
|
|
Description: "Verified city updates",
|
|
PublicUsername: "daily_news"),
|
|
CancellationToken.None);
|
|
|
|
var summary = ControllerTestFixture.ValueOf(response);
|
|
var stored = dbContext.Chats.Single(x => x.Id == channel.Id);
|
|
Assert.Equal("Daily News", summary.Title);
|
|
Assert.Equal("Daily News", stored.Title);
|
|
Assert.Equal("Verified city updates", stored.Description);
|
|
Assert.Equal("daily_news", stored.PublicUsername);
|
|
Assert.Contains(dbContext.ModerationLogEntries, x =>
|
|
x.ChatId == channel.Id &&
|
|
x.ActorUserId == owner.Id &&
|
|
x.Action == "chat.profile.updated");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateChatState_CanClearChannelDescriptionAndPublicUsername()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var channel = CreateChannel(owner);
|
|
channel.Description = "Old description";
|
|
channel.PublicUsername = "old_news";
|
|
dbContext.AddRange(owner, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.UpdateChatState(
|
|
channel.Id,
|
|
new ChatStateRequest(ClearDescription: true, ClearPublicUsername: true),
|
|
CancellationToken.None);
|
|
|
|
ControllerTestFixture.ValueOf(response);
|
|
var stored = dbContext.Chats.Single(x => x.Id == channel.Id);
|
|
Assert.Null(stored.Description);
|
|
Assert.Null(stored.PublicUsername);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateChatState_DeniesMemberProfileUpdate()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var member = CreateUser("member");
|
|
var channel = CreateChannel(owner);
|
|
channel.Members.Add(new ChatMember
|
|
{
|
|
ChatId = channel.Id,
|
|
Chat = channel,
|
|
UserId = member.Id,
|
|
User = member,
|
|
Role = ChatMemberRole.Member
|
|
});
|
|
dbContext.AddRange(owner, member, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, member.Id);
|
|
var response = await controller.UpdateChatState(
|
|
channel.Id,
|
|
new ChatStateRequest(Title: "Member title"),
|
|
CancellationToken.None);
|
|
|
|
var forbidden = Assert.IsType<Microsoft.AspNetCore.Mvc.ObjectResult>(response.Result);
|
|
Assert.Equal(StatusCodes.Status403Forbidden, forbidden.StatusCode);
|
|
Assert.Equal("Only admins can update chat profile.", forbidden.Value);
|
|
Assert.Equal("News", dbContext.Chats.Single(x => x.Id == channel.Id).Title);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateChatState_RejectsTakenPublicUsername()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var otherOwner = CreateUser("other");
|
|
var channel = CreateChannel(owner);
|
|
var otherChannel = CreateChannel(otherOwner);
|
|
otherChannel.PublicUsername = "taken_name";
|
|
dbContext.AddRange(owner, otherOwner, channel, otherChannel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.UpdateChatState(
|
|
channel.Id,
|
|
new ChatStateRequest(PublicUsername: "taken_name"),
|
|
CancellationToken.None);
|
|
|
|
var conflict = Assert.IsType<Microsoft.AspNetCore.Mvc.ConflictObjectResult>(response.Result);
|
|
Assert.Equal("Public username is already taken.", conflict.Value);
|
|
Assert.Equal("news", dbContext.Chats.Single(x => x.Id == channel.Id).PublicUsername);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TransferOwnership_MovesOwnerRoleToChannelMember()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var member = CreateUser("member");
|
|
var channel = CreateChannel(owner);
|
|
channel.Members.Add(new ChatMember
|
|
{
|
|
ChatId = channel.Id,
|
|
Chat = channel,
|
|
UserId = member.Id,
|
|
User = member,
|
|
IsOwner = false,
|
|
Role = ChatMemberRole.Member
|
|
});
|
|
dbContext.AddRange(owner, member, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.TransferOwnership(channel.Id, member.Id, CancellationToken.None);
|
|
|
|
var details = ControllerTestFixture.ValueOf(response);
|
|
Assert.Equal(member.Id, dbContext.Chats.Single(x => x.Id == channel.Id).CreatedById);
|
|
var ownerMember = Assert.Single(dbContext.ChatMembers, x => x.ChatId == channel.Id && x.UserId == owner.Id);
|
|
var targetMember = Assert.Single(dbContext.ChatMembers, x => x.ChatId == channel.Id && x.UserId == member.Id);
|
|
Assert.False(ownerMember.IsOwner);
|
|
Assert.Equal(ChatMemberRole.Admin, ownerMember.Role);
|
|
Assert.True(ownerMember.CanPostMessages);
|
|
Assert.True(targetMember.IsOwner);
|
|
Assert.Equal(ChatMemberRole.Owner, targetMember.Role);
|
|
Assert.True(targetMember.CanInviteUsers);
|
|
Assert.Single(dbContext.ChatMembers.Where(x => x.ChatId == channel.Id && (x.IsOwner || x.Role == ChatMemberRole.Owner)));
|
|
var returnedTarget = Assert.Single(details.Members ?? [], x => x.User.Id == member.Id);
|
|
Assert.True(returnedTarget.IsOwner);
|
|
Assert.Equal(ChatMemberRole.Owner, returnedTarget.Role);
|
|
Assert.Contains(dbContext.ModerationLogEntries, x =>
|
|
x.ChatId == channel.Id &&
|
|
x.ActorUserId == owner.Id &&
|
|
x.TargetId == member.Id &&
|
|
x.Action == "member.owner.transferred");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TransferOwnership_RejectsAdminActor()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var admin = CreateUser("admin");
|
|
var member = CreateUser("member");
|
|
var channel = CreateChannel(owner);
|
|
channel.Members.Add(new ChatMember
|
|
{
|
|
ChatId = channel.Id,
|
|
Chat = channel,
|
|
UserId = admin.Id,
|
|
User = admin,
|
|
IsOwner = false,
|
|
Role = ChatMemberRole.Admin,
|
|
CanPinMessages = true,
|
|
CanDeleteMessages = true,
|
|
CanInviteUsers = true,
|
|
CanPostMessages = true
|
|
});
|
|
channel.Members.Add(new ChatMember
|
|
{
|
|
ChatId = channel.Id,
|
|
Chat = channel,
|
|
UserId = member.Id,
|
|
User = member,
|
|
IsOwner = false,
|
|
Role = ChatMemberRole.Member
|
|
});
|
|
dbContext.AddRange(owner, admin, member, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, admin.Id);
|
|
var response = await controller.TransferOwnership(channel.Id, member.Id, CancellationToken.None);
|
|
|
|
var forbidden = Assert.IsType<Microsoft.AspNetCore.Mvc.ObjectResult>(response.Result);
|
|
Assert.Equal(Microsoft.AspNetCore.Http.StatusCodes.Status403Forbidden, forbidden.StatusCode);
|
|
Assert.Equal("Only owner can transfer ownership.", forbidden.Value);
|
|
var ownerMember = Assert.Single(dbContext.ChatMembers, x => x.ChatId == channel.Id && x.UserId == owner.Id);
|
|
Assert.True(ownerMember.IsOwner);
|
|
Assert.Equal(ChatMemberRole.Owner, ownerMember.Role);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TransferOwnership_RejectsBotTarget()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var bot = CreateUser("bot", isBot: true);
|
|
var channel = CreateChannel(owner);
|
|
channel.Members.Add(new ChatMember
|
|
{
|
|
ChatId = channel.Id,
|
|
Chat = channel,
|
|
UserId = bot.Id,
|
|
User = bot,
|
|
IsOwner = false,
|
|
Role = ChatMemberRole.Admin,
|
|
CanPinMessages = true,
|
|
CanDeleteMessages = true,
|
|
CanInviteUsers = true,
|
|
CanPostMessages = true
|
|
});
|
|
dbContext.AddRange(owner, bot, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.TransferOwnership(channel.Id, bot.Id, CancellationToken.None);
|
|
|
|
var badRequest = Assert.IsType<Microsoft.AspNetCore.Mvc.BadRequestObjectResult>(response.Result);
|
|
Assert.Equal("Bot cannot become chat owner.", badRequest.Value);
|
|
var ownerMember = Assert.Single(dbContext.ChatMembers, x => x.ChatId == channel.Id && x.UserId == owner.Id);
|
|
var botMember = Assert.Single(dbContext.ChatMembers, x => x.ChatId == channel.Id && x.UserId == bot.Id);
|
|
Assert.True(ownerMember.IsOwner);
|
|
Assert.False(botMember.IsOwner);
|
|
Assert.Equal(ChatMemberRole.Admin, botMember.Role);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteChat_BlocksOwnerLeavingChannelWithoutDeletingForEveryone()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var member = CreateUser("member");
|
|
var channel = CreateChannel(owner);
|
|
channel.Members.Add(new ChatMember
|
|
{
|
|
ChatId = channel.Id,
|
|
Chat = channel,
|
|
UserId = member.Id,
|
|
User = member,
|
|
IsOwner = false,
|
|
Role = ChatMemberRole.Member
|
|
});
|
|
dbContext.AddRange(owner, member, channel);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.DeleteChat(channel.Id, forEveryone: false, CancellationToken.None);
|
|
|
|
var badRequest = Assert.IsType<Microsoft.AspNetCore.Mvc.BadRequestObjectResult>(response);
|
|
Assert.Equal("Owner must transfer ownership or delete the chat for everyone.", badRequest.Value);
|
|
Assert.Equal(2, dbContext.ChatMembers.Count(x => x.ChatId == channel.Id));
|
|
Assert.True(dbContext.Chats.Any(x => x.Id == channel.Id));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateMessage_AllowsChannelAdminWithPostPermissionToEditChannelPost()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var admin = CreateUser("admin");
|
|
var channel = CreateChannel(owner);
|
|
channel.Members.Add(new ChatMember
|
|
{
|
|
ChatId = channel.Id,
|
|
Chat = channel,
|
|
UserId = admin.Id,
|
|
User = admin,
|
|
Role = ChatMemberRole.Admin,
|
|
CanPostMessages = true
|
|
});
|
|
var message = CreateTextMessage(channel, owner, "Original");
|
|
dbContext.AddRange(owner, admin, channel, message);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, admin.Id);
|
|
var response = await controller.UpdateMessage(
|
|
channel.Id,
|
|
message.Id,
|
|
new UpdateMessageRequest("Updated by admin"),
|
|
CancellationToken.None);
|
|
|
|
var updated = ControllerTestFixture.ValueOf(response);
|
|
Assert.Equal("Updated by admin", updated.Text);
|
|
Assert.NotNull(dbContext.Messages.Single(x => x.Id == message.Id).EditedAt);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateMessage_DeniesChannelMemberWithoutPostPermission()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var member = CreateUser("member");
|
|
var channel = CreateChannel(owner);
|
|
channel.Members.Add(new ChatMember
|
|
{
|
|
ChatId = channel.Id,
|
|
Chat = channel,
|
|
UserId = member.Id,
|
|
User = member,
|
|
Role = ChatMemberRole.Member,
|
|
CanPostMessages = false
|
|
});
|
|
var message = CreateTextMessage(channel, owner, "Original");
|
|
dbContext.AddRange(owner, member, channel, message);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, member.Id);
|
|
var response = await controller.UpdateMessage(
|
|
channel.Id,
|
|
message.Id,
|
|
new UpdateMessageRequest("Updated by admin"),
|
|
CancellationToken.None);
|
|
|
|
var forbidden = Assert.IsType<Microsoft.AspNetCore.Mvc.ObjectResult>(response.Result);
|
|
Assert.Equal(StatusCodes.Status403Forbidden, forbidden.StatusCode);
|
|
Assert.Equal("You cannot edit this message.", forbidden.Value);
|
|
Assert.Equal("Original", dbContext.Messages.Single(x => x.Id == message.Id).Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DownloadAttachment_DoesNotServeDeletedMessageAttachment()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var channel = CreateChannel(owner);
|
|
var message = CreateMessageWithAttachment(channel, owner, deletedAt: DateTimeOffset.UtcNow);
|
|
dbContext.AddRange(owner, channel, message);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.DownloadAttachment(channel.Id, message.Attachments.Single().Id, CancellationToken.None);
|
|
|
|
Assert.IsType<Microsoft.AspNetCore.Mvc.NotFoundResult>(response);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UploadAttachmentAlbum_RejectsSingleFile()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var group = CreateGroup(owner);
|
|
dbContext.AddRange(owner, group);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.UploadAttachmentAlbum(
|
|
group.Id,
|
|
new UploadAttachmentAlbumRequest
|
|
{
|
|
Files = [CreateFormFile([1, 2, 3], "photo.jpg", "image/jpeg")]
|
|
},
|
|
CancellationToken.None);
|
|
|
|
var badRequest = Assert.IsType<Microsoft.AspNetCore.Mvc.BadRequestObjectResult>(response.Result);
|
|
Assert.Equal("Albums must contain at least 2 files.", badRequest.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UploadAttachmentAlbum_RejectsDocumentFile()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var group = CreateGroup(owner);
|
|
dbContext.AddRange(owner, group);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.UploadAttachmentAlbum(
|
|
group.Id,
|
|
new UploadAttachmentAlbumRequest
|
|
{
|
|
Files =
|
|
[
|
|
CreateFormFile([1, 2, 3], "photo.jpg", "image/jpeg"),
|
|
CreateFormFile([0x25, 0x50, 0x44, 0x46], "manual.pdf", "application/pdf")
|
|
]
|
|
},
|
|
CancellationToken.None);
|
|
|
|
var badRequest = Assert.IsType<Microsoft.AspNetCore.Mvc.BadRequestObjectResult>(response.Result);
|
|
Assert.Equal("Albums accept only image and video files.", badRequest.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DownloadAttachment_DoesNotServeExpiredMessageAttachment()
|
|
{
|
|
await using var database = new SqliteTestDatabase();
|
|
await using var dbContext = await database.CreateContextAsync();
|
|
var owner = CreateUser("owner");
|
|
var channel = CreateChannel(owner);
|
|
var message = CreateMessageWithAttachment(channel, owner, expiresAt: DateTimeOffset.UtcNow.AddSeconds(-1));
|
|
dbContext.AddRange(owner, channel, message);
|
|
await dbContext.SaveChangesAsync();
|
|
|
|
var controller = ControllerTestFixture.CreateChatsController(dbContext, owner.Id);
|
|
var response = await controller.DownloadAttachment(channel.Id, message.Attachments.Single().Id, CancellationToken.None);
|
|
|
|
Assert.IsType<Microsoft.AspNetCore.Mvc.NotFoundResult>(response);
|
|
}
|
|
|
|
private static User CreateUser(string username, bool isBot = false) =>
|
|
new()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Username = username,
|
|
NormalizedUsername = username.ToUpperInvariant(),
|
|
DisplayName = username,
|
|
PasswordHash = "test",
|
|
IsBot = isBot
|
|
};
|
|
|
|
private static Chat CreateChannel(User owner)
|
|
{
|
|
var channel = new Chat
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Type = ChatType.Channel,
|
|
Title = "News",
|
|
Description = "Public news channel",
|
|
PublicUsername = "news",
|
|
CreatedById = owner.Id,
|
|
CreatedBy = owner,
|
|
CreatedAt = new DateTimeOffset(2026, 1, 1, 9, 0, 0, TimeSpan.Zero),
|
|
LastActivityAt = new DateTimeOffset(2026, 1, 1, 9, 0, 0, TimeSpan.Zero)
|
|
};
|
|
channel.Members.Add(new ChatMember
|
|
{
|
|
ChatId = channel.Id,
|
|
Chat = channel,
|
|
UserId = owner.Id,
|
|
User = owner,
|
|
IsOwner = true,
|
|
Role = ChatMemberRole.Owner,
|
|
CanPinMessages = true,
|
|
CanDeleteMessages = true,
|
|
CanInviteUsers = true,
|
|
CanPostMessages = true
|
|
});
|
|
|
|
return channel;
|
|
}
|
|
|
|
private static Chat CreateGroup(User owner)
|
|
{
|
|
var group = new Chat
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Type = ChatType.Group,
|
|
Title = "Group",
|
|
CreatedById = owner.Id,
|
|
CreatedBy = owner,
|
|
CreatedAt = new DateTimeOffset(2026, 1, 1, 9, 0, 0, TimeSpan.Zero),
|
|
LastActivityAt = new DateTimeOffset(2026, 1, 1, 9, 0, 0, TimeSpan.Zero)
|
|
};
|
|
group.Members.Add(new ChatMember
|
|
{
|
|
ChatId = group.Id,
|
|
Chat = group,
|
|
UserId = owner.Id,
|
|
User = owner,
|
|
IsOwner = true,
|
|
Role = ChatMemberRole.Owner,
|
|
CanPinMessages = true,
|
|
CanDeleteMessages = true,
|
|
CanInviteUsers = true,
|
|
CanPostMessages = true
|
|
});
|
|
|
|
return group;
|
|
}
|
|
|
|
private static FormFile CreateFormFile(byte[] content, string fileName, string contentType)
|
|
{
|
|
var stream = new MemoryStream(content);
|
|
return new FormFile(stream, 0, stream.Length, "files", fileName)
|
|
{
|
|
Headers = new HeaderDictionary(),
|
|
ContentType = contentType
|
|
};
|
|
}
|
|
|
|
private static Message CreateTextMessage(Chat chat, User sender, string text)
|
|
{
|
|
var message = new Message
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
ChatId = chat.Id,
|
|
Chat = chat,
|
|
SenderId = sender.Id,
|
|
Sender = sender,
|
|
Text = text,
|
|
SentAt = DateTimeOffset.UtcNow
|
|
};
|
|
chat.Messages.Add(message);
|
|
return message;
|
|
}
|
|
|
|
private static Message CreateMessageWithAttachment(
|
|
Chat chat,
|
|
User sender,
|
|
DateTimeOffset? deletedAt = null,
|
|
DateTimeOffset? expiresAt = null)
|
|
{
|
|
var message = new Message
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
ChatId = chat.Id,
|
|
Chat = chat,
|
|
SenderId = sender.Id,
|
|
Sender = sender,
|
|
Text = string.Empty,
|
|
SentAt = DateTimeOffset.UtcNow,
|
|
DeletedAt = deletedAt,
|
|
ExpiresAt = expiresAt
|
|
};
|
|
message.Attachments.Add(new MessageAttachment
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
MessageId = message.Id,
|
|
Message = message,
|
|
OriginalFileName = "document.pdf",
|
|
ContentType = "application/pdf",
|
|
StoragePath = "missing.pdf"
|
|
});
|
|
|
|
return message;
|
|
}
|
|
}
|