Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allowing facilitators to move team members to observers #165

Merged
merged 1 commit into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
331 changes: 169 additions & 162 deletions PointerStar/Client/Pages/Room.razor

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion PointerStar/Client/ViewModels/RoomViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public async Task OnClickClipboardAsync(string? url)

private void RoomStateUpdated(object? sender, RoomState roomState)
{
//Intentionally going to the field here to dodge recurssive calls.
//Intentionally going to the field here to dodge recursive calls.
//The INPC from RoomState will update the state
#pragma warning disable MVVMTK0034 // Direct field reference to [ObservableProperty] backing field
_votesShown = roomState.VotesShown;
Expand Down Expand Up @@ -169,6 +169,14 @@ public async Task ResetVotesAsync()
}
}

public async Task RemoveUserAsync(Guid userId)
{
if (RoomHubConnection.IsConnected)
{
await RoomHubConnection.RemoveUserAsync(userId);
}
}

private async Task ProcessVotingTimer(CancellationToken token)
{
using PeriodicTimer votingTimer = new(TimeSpan.FromSeconds(0.5));
Expand Down
8 changes: 4 additions & 4 deletions PointerStar/Server/Controllers/RoomController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ namespace PointerStar.Server.Controllers;
public class RoomController : ControllerBase
{
private static readonly Random Random = new();
private Hashids Hashids { get; }
private Hashids HashIds { get; }
private IRoomManager RoomManager { get; }

public RoomController(Hashids hashids, IRoomManager roomManager)
public RoomController(Hashids hashIds, IRoomManager roomManager)
{
Hashids = hashids ?? throw new ArgumentNullException(nameof(hashids));
HashIds = hashIds ?? throw new ArgumentNullException(nameof(hashIds));
RoomManager = roomManager ?? throw new ArgumentNullException(nameof(roomManager));
}


[HttpGet("Generate")]
public string Generate()
=> Hashids.Encode(Random.Next());
=> HashIds.Encode(Random.Next());

[HttpGet("GetNewUserRole/{RoomId}")]
public Task<Role> GetNewUserRole(string roomId)
Expand Down
10 changes: 10 additions & 0 deletions PointerStar/Server/Hubs/RoomHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,14 @@ public async Task ResetVotesAsync()
await Clients.Groups(roomId).SendAsync(RoomHubConnection.RoomUpdatedMethodName, roomState);
}
}

[HubMethodName(RoomHubConnection.RemoveUserMethodName)]
public async Task RemoveUserAsync(Guid userId)
{
RoomState? roomState = await RoomManager.RemoveUserAsync(userId, Context.ConnectionId);
if (roomState?.RoomId is { } roomId)
{
await Clients.Groups(roomId).SendAsync(RoomHubConnection.RoomUpdatedMethodName, roomState);
}
}
}
1 change: 1 addition & 0 deletions PointerStar/Server/Room/IRoomManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public interface IRoomManager
Task<RoomState?> UpdateUserAsync(UserOptions userOptions, string connectionId);
Task<RoomState?> SubmitVoteAsync(string vote, string connectionId);
Task<RoomState?> ResetVotesAsync(string connectionId);
Task<RoomState?> RemoveUserAsync(Guid userId, string connectionId);

Task<Role> GetNewUserRoleAsync(string roomId);
}
47 changes: 32 additions & 15 deletions PointerStar/Server/Room/InMemoryRoomManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ public Task<RoomState> AddUserToRoomAsync(string roomId, User user, string conne

public Task<RoomState?> DisconnectAsync(string connectionId)
{
return WithConnection(connectionId, (room, userId) =>
return WithConnection(connectionId, (room, currentUser) =>
{
User[] users = room.Users.Where(x => x.Id != userId).ToArray();
User[] users = room.Users.Where(x => x.Id != currentUser.Id).ToArray();
if (users.Any())
{
return room with { Users = users };
Expand All @@ -46,10 +46,10 @@ public Task<RoomState> AddUserToRoomAsync(string roomId, User user, string conne

public Task<RoomState?> UpdateRoomAsync(RoomOptions roomOptions, string connectionId)
{
return WithConnection(connectionId, (room, userId) =>
return WithConnection(connectionId, (room, currentUser) =>
{
//Only allow facilitators to change the room options
if (room.Users.FirstOrDefault(x => x.Id == userId)?.Role == Role.Facilitator)
if (currentUser.Role == Role.Facilitator)
{
if (roomOptions.AutoShowVotes is { } autoShowVotes)
{
Expand All @@ -76,11 +76,11 @@ public Task<RoomState> AddUserToRoomAsync(string roomId, User user, string conne

public Task<RoomState?> UpdateUserAsync(UserOptions userOptions, string connectionId)
{
return WithConnection(connectionId, (room, userId) =>
return WithConnection(connectionId, (room, currentUser) =>
{
User[] users = room.Users.Select(x =>
{
if (x.Id == userId)
if (x.Id == currentUser.Id)
{
if (userOptions.Name is { } name)
{
Expand All @@ -99,11 +99,11 @@ public Task<RoomState> AddUserToRoomAsync(string roomId, User user, string conne

public Task<RoomState?> SubmitVoteAsync(string vote, string connectionId)
{
return WithConnection(connectionId, (room, userId) =>
return WithConnection(connectionId, (room, currentUser) =>
{
var roomState = room with
{
Users = room.Users.Select(u => u.Id == userId ? u with
Users = room.Users.Select(u => u.Id == currentUser.Id ? u with
{
OriginalVote = room.VotesShown ? u.OriginalVote : vote,
Vote = vote
Expand All @@ -123,10 +123,8 @@ public Task<RoomState> AddUserToRoomAsync(string roomId, User user, string conne

public Task<RoomState?> ResetVotesAsync(string connectionId)
{
return WithConnection(connectionId, (room, userId) =>
return WithConnection(connectionId, (room, currentUser) =>
{
User? currentUser = room.Users.FirstOrDefault(x => x.Id == userId);
if (currentUser is null) return null;
if (currentUser.Role == Role.Facilitator)
{
User[] users = room.Users.Select(u => u with { Vote = null }).ToArray();
Expand Down Expand Up @@ -154,12 +152,31 @@ public Task<Role> GetNewUserRoleAsync(string roomId)
});
}

private Task<RoomState?> WithConnection(string connectionId, Func<RoomState, Guid, RoomState?> updateRoom)
public Task<RoomState?> RemoveUserAsync(Guid userId, string connectionId)
{
return WithConnection(connectionId, (room, currentUser) =>
{
if (currentUser.Role != Role.Facilitator)
{
return room;
}
return room with
{
Users = room.Users.Select(u => u.Id == userId ? u with
{
Role = Role.Observer
} : u).ToArray()
};
});
}


private Task<RoomState?> WithConnection(string connectionId, Func<RoomState, User, RoomState?> updateRoom)
{
if (ConnectionsToRoom.TryGetValue(connectionId, out string? roomId) &&
ConnectionsToUser.TryGetValue(connectionId, out User? user))
{
return WithExistingRoom(roomId, room => updateRoom(room, user.Id));
return WithExistingRoom(roomId, room => updateRoom(room, user));
}
return Task.FromResult<RoomState?>(null);
}
Expand Down Expand Up @@ -211,8 +228,8 @@ private static bool ShouldShowVotes(RoomState roomState)
{
if (roomState.AutoShowVotes)
{
var teamMemebers = roomState.TeamMemebers;
if (teamMemebers.Any() && teamMemebers.All(x => x.Vote is not null))
var teamMembers = roomState.TeamMemebers;
if (teamMembers.Any() && teamMembers.All(x => x.Vote is not null))
{
return true;
}
Expand Down
1 change: 1 addition & 0 deletions PointerStar/Shared/IRoomHubConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public interface IRoomHubConnection
Task UpdateRoomAsync(RoomOptions roomOptions);
Task UpdateUserAsync(UserOptions userOptions);
Task ResetVotesAsync();
Task RemoveUserAsync(Guid userId);
}
3 changes: 3 additions & 0 deletions PointerStar/Shared/RoomHubConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class RoomHubConnection : IRoomHubConnection
public const string UpdateRoomMethodName = "UpdateRoom";
public const string UpdateUserMethodName = "UpdateUser";
public const string ResetVotesMethodName = "ResetVotes";
public const string RemoveUserMethodName = "RemoveUser";
public const string RoomUpdatedMethodName = "RoomUpdated";

public event EventHandler<RoomState>? RoomStateUpdated;
Expand Down Expand Up @@ -68,4 +69,6 @@ public Task UpdateUserAsync(UserOptions userOptions)

public Task ResetVotesAsync()
=> HubConnection.InvokeAsync(ResetVotesMethodName);
public Task RemoveUserAsync(Guid userId)
=> HubConnection.InvokeAsync(RemoveUserMethodName, userId);
}
15 changes: 15 additions & 0 deletions Tests/PointerStar.Client.Tests/ViewModels/RoomViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,21 @@ public async Task ShowUserDialog_WithNewUser_JoinsRooms()
mocker.Verify<IRoomHubConnection>(x => x.JoinRoomAsync("RoomId", It.Is<User>(u => u.Name == "Test User" && u.Role == Role.Observer)), Times.Once);
}

[Fact]
public async Task RemoveUserAsync_WithUserId_RemovesUser()
{
AutoMocker mocker = new();
mocker.Setup<IRoomHubConnection, bool>(x => x.IsConnected).Returns(true);
User teamMember = new(Guid.NewGuid(), "Team Member") { Role = Role.TeamMember };
RoomState roomState = new(Guid.NewGuid().ToString(), new[] { teamMember });
RoomViewModel viewModel = mocker.CreateInstance<RoomViewModel>();
WithRoomState(mocker, roomState);

await viewModel.RemoveUserAsync(teamMember.Id);

mocker.Verify<IRoomHubConnection>(x => x.RemoveUserAsync(teamMember.Id), Times.Once);
}

private static void WithUserDialog(AutoMocker mocker, string? name, Guid? selectedRoleId)
{
UserDialogViewModel dialogViewModel = mocker.CreateInstance<UserDialogViewModel>();
Expand Down
64 changes: 57 additions & 7 deletions Tests/PointerStar.Server.Tests/Room/RoomManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public async Task SubmitVoteAsync_AfterVotesShown_UpdatesVote()
}

[Fact]
public async Task SubmitVoteAsync_WithAutoShowVotes_ShowsVotesWhenAllTeamMemebersHaveVoted()
public async Task SubmitVoteAsync_WithAutoShowVotes_ShowsVotesWhenAllTeamMembersHaveVoted()
{
AutoMocker mocker = new();
string facilitator = Guid.NewGuid().ToString();
Expand Down Expand Up @@ -194,7 +194,7 @@ public async Task UpdateRoomAsync_WithExistingFacilitator_UpdatesRoom(
}

[Fact]
public async Task UpateRoomAsync_WithTeamMemberUser_DoesNotUpdateRoom()
public async Task UpdateRoomAsync_WithTeamMemberUser_DoesNotUpdateRoom()
{
AutoMocker mocker = new();

Expand Down Expand Up @@ -333,7 +333,7 @@ public async Task GetNewUserRoleAsync_WithNewRoom_ReturnsFacilitator()
AutoMocker mocker = new();
IRoomManager sut = mocker.CreateInstance<TRoomManager>();

Role role= await sut.GetNewUserRoleAsync("unkownId");
Role role= await sut.GetNewUserRoleAsync("unknownId");

Assert.Equal(Role.Facilitator, role);
}
Expand All @@ -344,7 +344,7 @@ public async Task GetNewUserRoleAsync_WithoutFacilitatorsInTheRoom_ReturnsFacili
AutoMocker mocker = new();
IRoomManager sut = mocker.CreateInstance<TRoomManager>();
string roomId = Guid.NewGuid().ToString();
User user = new(Guid.NewGuid(), "Team Memeber");
User user = new(Guid.NewGuid(), "Team Member");
_ = await sut.AddUserToRoomAsync(roomId, user, Guid.NewGuid().ToString());

Role role = await sut.GetNewUserRoleAsync(roomId);
Expand All @@ -353,7 +353,7 @@ public async Task GetNewUserRoleAsync_WithoutFacilitatorsInTheRoom_ReturnsFacili
}

[Fact]
public async Task GetNewUserRoleAsync_WithExistingFacilitator_ReturnsTeamMemeber()
public async Task GetNewUserRoleAsync_WithExistingFacilitator_ReturnsTeamMember()
{
AutoMocker mocker = new();
IRoomManager sut = mocker.CreateInstance<TRoomManager>();
Expand All @@ -378,14 +378,64 @@ public async Task ResetVotes_WithFacilitator_StopsVotingTimer()

Assert.True(roomState?.VoteStartTime.HasValue);
}


[Fact]
public async Task RemoveUserAsync_WithFacilitator_MovesTeamMemberToObserver()
{
AutoMocker mocker = new();
string facilitator = Guid.NewGuid().ToString();
string teamMember = Guid.NewGuid().ToString();
IRoomManager sut = mocker.CreateInstance<TRoomManager>();
RoomState room = await CreateRoom(sut, facilitator, teamMember);
User userToRemove = room.TeamMemebers.Single();

RoomState? roomState = await sut.RemoveUserAsync(userToRemove.Id, facilitator);

Assert.NotNull(roomState);
Assert.Empty(roomState.TeamMemebers);
Assert.Equal(userToRemove.Id, roomState.Observers.Select(x => x.Id).Single());
}

[Fact]
public async Task RemoveUserAsync_WithTeamMember_DoesNothing()
{
AutoMocker mocker = new();
string facilitator = Guid.NewGuid().ToString();
string teamMember1 = Guid.NewGuid().ToString();
string teamMember2 = Guid.NewGuid().ToString();
IRoomManager sut = mocker.CreateInstance<TRoomManager>();
RoomState room = await CreateRoom(sut, facilitator, teamMember1, teamMember2);
User userToRemove = room.TeamMemebers.First();

RoomState? roomState = await sut.RemoveUserAsync(userToRemove.Id, teamMember2);

Assert.NotNull(roomState);
Assert.Equal(2, roomState.TeamMemebers.Count);
}

[Fact]
public async Task RemoveUserAsync_WithUnknownUserId_DoesNothing()
{
AutoMocker mocker = new();
string facilitator = Guid.NewGuid().ToString();
string teamMember = Guid.NewGuid().ToString();
IRoomManager sut = mocker.CreateInstance<TRoomManager>();
RoomState room = await CreateRoom(sut, facilitator, teamMember);

RoomState? roomState = await sut.RemoveUserAsync(Guid.NewGuid(), facilitator);

Assert.NotNull(roomState);
Assert.Equal(1, roomState.TeamMemebers.Count);
}


protected async Task<RoomState> CreateRoom(IRoomManager sut, params string[] connectionIds)
{
string roomId = Guid.NewGuid().ToString();
RoomState? rv = null;
for (int i = 0; i < connectionIds.Length; i++)
{
User user = new(Guid.NewGuid(), i == 0 ? $"Facilitator" : $"Team Memeber {i}")
User user = new(Guid.NewGuid(), i == 0 ? $"Facilitator" : $"Team Member {i}")
{
Role = i == 0 ? Role.Facilitator : Role.TeamMember
};
Expand Down