Move room admin condition to be usable in UnitTests and add tests

This commit is contained in:
aringenbach
2023-04-20 16:43:36 +02:00
parent a3f7d0433a
commit 0171e64638
5 changed files with 144 additions and 22 deletions
@@ -62,20 +62,34 @@ extension MockCompletionSuggestionScreenState: RoomMembersProviderProtocol {
}
extension MockCompletionSuggestionScreenState: CommandsProviderProtocol {
var isRoomAdmin: Bool { false }
func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) {
commands([
CommandsProviderCommand(name: "/ban",
parametersFormat: "<user-id> [<reason>]",
description: "Bans user with given id"),
description: "Bans user with given id",
requiresAdminPowerLevel: false),
CommandsProviderCommand(name: "/invite",
parametersFormat: "<user-id>",
description: "Invites user with given id to current room"),
description: "Invites user with given id to current room",
requiresAdminPowerLevel: false),
CommandsProviderCommand(name: "/join",
parametersFormat: "<room-address>",
description: "Joins room with given address"),
description: "Joins room with given address",
requiresAdminPowerLevel: false),
CommandsProviderCommand(name: "/op",
parametersFormat: "<user-id> <power-level>",
description: "Define the power level of a user",
requiresAdminPowerLevel: true),
CommandsProviderCommand(name: "/deop",
parametersFormat: "<user-id>",
description: "Deops user with given id",
requiresAdminPowerLevel: true),
CommandsProviderCommand(name: "/me",
parametersFormat: "<message>",
description: "Displays action")
description: "Displays action",
requiresAdminPowerLevel: false)
])
}
}
@@ -207,6 +207,7 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr
private let userID: String
var commands = MXKSlashCommand.allCases
var isRoomAdmin = false
init(room: MXRoom, userID: String) {
self.room = room
@@ -218,21 +219,13 @@ private class CompletionSuggestionCoordinatorCommandProvider: CommandsProviderPr
room.state { [weak self] state in
guard let self, let powerLevels = state?.powerLevels else { return }
// Note: for now only filter out `/op` and `/deop` (same as Element-Web),
// but we could use power level for ban/invite/etc to filter further.
let adminOnlyCommands: [MXKSlashCommand] = [.setUserPowerLevel, .resetUserPowerLevel]
let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: self.userID)
if RoomPowerLevel(rawValue: userPowerLevel) != .admin {
self.commands = self.commands.filter {
!adminOnlyCommands.contains($0)
}
}
isRoomAdmin = RoomPowerLevel(rawValue: userPowerLevel) == .admin
}
}
func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void) {
commands(self.commands.map { CommandsProviderCommand(name: $0.cmd, parametersFormat: $0.parametersFormat, description: $0.description) })
commands(self.commands.map { CommandsProviderCommand(name: $0.cmd, parametersFormat: $0.parametersFormat, description: $0.description, requiresAdminPowerLevel: $0.requiresAdminPowerLevel) })
}
}
@@ -266,4 +259,15 @@ private extension MXKSlashCommand {
return "Forces the current outbound group session in an encrypted room to be discarded"
}
}
// Note: for now only filter out `/op` and `/deop` (same as Element-Web),
// but we could use power level for ban/invite/etc to filter further.
var requiresAdminPowerLevel: Bool {
switch self {
case .setUserPowerLevel, .resetUserPowerLevel:
return true
default:
return false
}
}
}
@@ -28,6 +28,7 @@ struct CommandsProviderCommand {
let name: String
let parametersFormat: String
let description: String
let requiresAdminPowerLevel: Bool
}
class CompletionSuggestionUserID: NSObject {
@@ -41,6 +42,7 @@ protocol RoomMembersProviderProtocol {
}
protocol CommandsProviderProtocol {
var isRoomAdmin: Bool { get }
func fetchCommands(_ commands: @escaping ([CommandsProviderCommand]) -> Void)
}
@@ -159,7 +161,7 @@ class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
commandProvider.fetchCommands { [weak self] commands in
guard let self else { return }
self.suggestionItems = commands.map { command in
self.suggestionItems = commands.filtered(isRoomAdmin: self.commandProvider.isRoomAdmin).map { command in
CompletionSuggestionItem.command(value: CompletionSuggestionServiceCommandItem(
name: command.name,
parametersFormat: command.parametersFormat,
@@ -190,6 +192,13 @@ extension Array where Element == RoomMembersProviderMember {
}
}
extension Array where Element == CommandsProviderCommand {
func filtered(isRoomAdmin: Bool) -> Self {
guard !isRoomAdmin else { return self }
return filter { !$0.requiresAdminPowerLevel }
}
}
private enum SuggestionKey: Character {
case at = "@"
case slash = "/"
@@ -22,14 +22,18 @@ import XCTest
class CompletionSuggestionServiceTests: XCTestCase {
var service: CompletionSuggestionService!
var canMentionRoom = false
var isRoomAdmin = false
override func setUp() {
service = CompletionSuggestionService(roomMemberProvider: self,
commandProvider: self,
shouldDebounce: false)
canMentionRoom = false
isRoomAdmin = false
}
// MARK: - User suggestions
func testAlice() {
service.processTextMessage("@Al")
XCTAssertEqual(service.items.value.first?.asUser?.displayName, "Alice")
@@ -128,6 +132,85 @@ class CompletionSuggestionServiceTests: XCTestCase {
// Then the completion for a room mention should be shown.
XCTAssertEqual(service.items.value.first?.asUser?.userId, CompletionSuggestionUserID.room)
}
// MARK: - Command suggestions
func testJoin() {
service.processTextMessage("/jo")
XCTAssertEqual(service.items.value.first?.asCommand?.name, "/join")
service.processTextMessage("/joi")
XCTAssertEqual(service.items.value.first?.asCommand?.name, "/join")
service.processTextMessage("/join")
XCTAssertEqual(service.items.value.first?.asCommand?.name, "/join")
service.processTextMessage("/oin")
XCTAssertEqual(service.items.value.first?.asCommand?.name, "/join")
}
func testInvite() {
service.processTextMessage("/inv")
XCTAssertEqual(service.items.value.first?.asCommand?.name, "/invite")
service.processTextMessage("/invite")
XCTAssertEqual(service.items.value.first?.asCommand?.name, "/invite")
service.processTextMessage("/vite")
XCTAssertEqual(service.items.value.first?.asCommand?.name, "/invite")
}
func testMultipleResults() {
service.processTextMessage("/in")
XCTAssertEqual(
service.items.value.compactMap { $0.asCommand?.name },
["/invite", "/join"]
)
}
func testDoubleSlashDontTrigger() {
service.processTextMessage("//")
XCTAssertTrue(service.items.value.isEmpty)
}
func testNonLeadingSlashCommandDontTrigger() {
service.processTextMessage("test /joi")
XCTAssertTrue(service.items.value.isEmpty)
}
func testAdminCommandsAreNotAvailable() {
isRoomAdmin = false
service.processTextMessage("/op")
XCTAssertTrue(service.items.value.isEmpty)
}
func testAdminCommandsAreAvailable() {
isRoomAdmin = true
service.processTextMessage("/op")
XCTAssertEqual(service.items.value.compactMap { $0.asCommand?.name }, ["/op", "/deop"])
}
func testDisplayAllCommandsAsStandardUser() {
isRoomAdmin = false
service.processTextMessage("/")
XCTAssertEqual(
service.items.value.compactMap { $0.asCommand?.name },
["/ban", "/invite", "/join", "/me"]
)
}
func testDisplayAllCommandsAsAdmin() {
isRoomAdmin = true
service.processTextMessage("/")
XCTAssertEqual(
service.items.value.compactMap { $0.asCommand?.name },
["/ban", "/invite", "/join", "/op", "/deop", "/me"]
)
}
}
extension CompletionSuggestionServiceTests: RoomMembersProviderProtocol {
@@ -146,16 +229,28 @@ extension CompletionSuggestionServiceTests: CommandsProviderProtocol {
commands([
CommandsProviderCommand(name: "/ban",
parametersFormat: "<user-id> [<reason>]",
description: "Bans user with given id"),
description: "Bans user with given id",
requiresAdminPowerLevel: false),
CommandsProviderCommand(name: "/invite",
parametersFormat: "<user-id>",
description: "Invites user with given id to current room"),
description: "Invites user with given id to current room",
requiresAdminPowerLevel: false),
CommandsProviderCommand(name: "/join",
parametersFormat: "<room-address>",
description: "Joins room with given address"),
description: "Joins room with given address",
requiresAdminPowerLevel: false),
CommandsProviderCommand(name: "/op",
parametersFormat: "<user-id> <power-level>",
description: "Define the power level of a user",
requiresAdminPowerLevel: true),
CommandsProviderCommand(name: "/deop",
parametersFormat: "<user-id>",
description: "Deops user with given id",
requiresAdminPowerLevel: true),
CommandsProviderCommand(name: "/me",
parametersFormat: "<message>",
description: "Displays action")
description: "Displays action",
requiresAdminPowerLevel: false)
])
}
}
@@ -34,13 +34,13 @@ struct CompletionSuggestionListWithInput: View {
var body: some View {
VStack(spacing: 0.0) {
CompletionSuggestionList(viewModel: viewModel.listViewModel.context)
TextField("Search for user", text: $inputText)
TextField("Search for user/command", text: $inputText)
.background(Color.white)
.onChange(of: inputText, perform: viewModel.callback)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding([.leading, .trailing])
.onAppear {
inputText = "@-" // Make the list show all available mock results
inputText = "@-" // Make the list show all available user mock results
}
}
}