Skip to content

Commit

Permalink
Add a read only view of linked player identities for their team owners
Browse files Browse the repository at this point in the history
  • Loading branch information
sussexrick committed Oct 29, 2023
1 parent 1b39a6a commit c2b79ea
Show file tree
Hide file tree
Showing 21 changed files with 358 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,7 @@ public async Task ReadPlayerIdentityByRoute_returns_identity_player_team_and_clu
Assert.Equal(identity.PlayerIdentityName, result.PlayerIdentityName);
Assert.Equal(identity.RouteSegment, result.RouteSegment);
Assert.Equal(identity.Player?.PlayerId, result.Player?.PlayerId);
Assert.Equal(identity.Player?.PlayerRoute, result.Player?.PlayerRoute);
Assert.Equal(identity.Team.TeamId, result.Team!.TeamId);
Assert.Equal(identity.Team.TeamName, result.Team!.TeamName);
Assert.Equal(identity.Team.TeamRoute, result.Team!.TeamRoute);
Expand Down
2 changes: 1 addition & 1 deletion Stoolball.Data.SqlServer/SqlServerPlayerDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ public async Task<List<PlayerIdentity>> ReadPlayerIdentities(PlayerFilter? filte
{
var results = await connection.QueryAsync<PlayerIdentity, Player, Team, Club, PlayerIdentity>(
$@"SELECT pi.PlayerIdentityId, pi.PlayerIdentityName, pi.RouteSegment,
pi.PlayerId,
pi.PlayerId, pi.PlayerRoute,
t.TeamId, tv.TeamName, t.TeamRoute,
c.ClubId, cv.ClubName, c.ClubRoute
FROM {Views.PlayerIdentity} pi INNER JOIN {Tables.Team} t ON pi.TeamId = t.TeamId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class StoolballRouteParserTests
[InlineData("https://example.org/teams/example123/edit/players", StoolballRouteType.EditPlayersForTeam)]
[InlineData("https://example.org/teams/example123/edit/players/some-player", StoolballRouteType.PlayerIdentityActions)]
[InlineData("https://example.org/teams/example123/edit/players/some-player/rename", StoolballRouteType.RenamePlayerIdentity)]
[InlineData("https://example.org/teams/example123/edit/players/some-player/statistics", StoolballRouteType.LinkedPlayersForIdentity)]
[InlineData("https://example.org/teams/example123/delete", StoolballRouteType.DeleteTeam)]
[InlineData("https://example.org/teams/example123/players", StoolballRouteType.PlayersForTeam)]
[InlineData("https://example.org/teams/example123/matches", StoolballRouteType.MatchesForTeam)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public async Task Unauthenticated_returns_view_model_without_player()
{
var result = await controller.Index();

Assert.Null(((LinkedPlayersForMemberViewModel)((ViewResult)result).Model).Player);
Assert.Null(((LinkedPlayersViewModel)((ViewResult)result).Model).Player);
}
}

Expand All @@ -54,7 +54,7 @@ public async Task Member_without_linked_player_returns_view_model_without_player

_playerDataSource.Verify(x => x.ReadPlayerByMemberKey(memberKey), Times.Once);
_playerDataSource.Verify(x => x.ReadPlayerByRoute(It.IsAny<string>(), null), Times.Never);
Assert.Null(((LinkedPlayersForMemberViewModel)((ViewResult)result).Model).Player);
Assert.Null(((LinkedPlayersViewModel)((ViewResult)result).Model).Player);
}
}

Expand All @@ -72,7 +72,7 @@ public async Task Member_with_linked_player_sets_player_in_view_model()
{
var result = await controller.Index();

Assert.Equal(player2, ((LinkedPlayersForMemberViewModel)((ViewResult)result).Model).Player);
Assert.Equal(player2, ((LinkedPlayersViewModel)((ViewResult)result).Model).Player);
}
}

Expand All @@ -83,7 +83,7 @@ public async Task Sets_breadcrumbs()
{
var result = await controller.Index();

var breadcrumbs = ((LinkedPlayersForMemberViewModel)((ViewResult)result).Model).Breadcrumbs;
var breadcrumbs = ((LinkedPlayersViewModel)((ViewResult)result).Model).Breadcrumbs;
Assert.Equal(2, breadcrumbs.Count);
Assert.Equal("Home", breadcrumbs[0].Name);
Assert.Equal("My account", breadcrumbs[1].Name);
Expand All @@ -97,7 +97,7 @@ public async Task Sets_page_title()
{
var result = await controller.Index();

Assert.False(string.IsNullOrWhiteSpace(((LinkedPlayersForMemberViewModel)((ViewResult)result).Model).Metadata.PageTitle));
Assert.False(string.IsNullOrWhiteSpace(((LinkedPlayersViewModel)((ViewResult)result).Model).Metadata.PageTitle));
}
}

Expand All @@ -110,7 +110,7 @@ public async Task Sets_PreferredNextRoute_from_referer_if_removing_domain()
{
var result = await controller.Index();

Assert.Equal("/from-referer", ((LinkedPlayersForMemberViewModel)((ViewResult)result).Model).PreferredNextRoute);
Assert.Equal("/from-referer", ((LinkedPlayersViewModel)((ViewResult)result).Model).PreferredNextRoute);
}
}

Expand All @@ -121,7 +121,7 @@ public async Task Sets_PreferredNextRoute_to_account_if_no_referer()
{
var result = await controller.Index();

Assert.Equal(Constants.Pages.AccountUrl, ((LinkedPlayersForMemberViewModel)((ViewResult)result).Model).PreferredNextRoute);
Assert.Equal(Constants.Pages.AccountUrl, ((LinkedPlayersViewModel)((ViewResult)result).Model).PreferredNextRoute);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
using Stoolball.Data.Abstractions;
using Stoolball.Security;
using Stoolball.Statistics;
using Stoolball.Teams;
using Stoolball.Web.Navigation;
using Stoolball.Web.Statistics.Models;
using Stoolball.Web.Teams;
using Xunit;

namespace Stoolball.Web.UnitTests.Teams
{
public class LinkedPlayersForIdentityControllerTests : UmbracoBaseTest
{
private readonly Mock<IPlayerDataSource> _playerDataSource = new();
private readonly Mock<IAuthorizationPolicy<Team>> _authorizationPolicy = new();
private readonly Mock<ITeamBreadcrumbBuilder> _breadcrumbBuilder = new();

private LinkedPlayersForIdentityController CreateController()
{
return new LinkedPlayersForIdentityController(
Mock.Of<ILogger<LinkedPlayersForIdentityController>>(),
CompositeViewEngine.Object,
UmbracoContextAccessor.Object,
_authorizationPolicy.Object,
_playerDataSource.Object,
_breadcrumbBuilder.Object)
{
ControllerContext = ControllerContext
};
}

private PlayerIdentity SetupMocks()
{
var identity = new PlayerIdentity
{
PlayerIdentityId = Guid.NewGuid(),
PlayerIdentityName = "John Smith",
Team = new Team(),
Player = new Player { PlayerId = Guid.NewGuid(), PlayerRoute = "/players/example-player" }
};
_playerDataSource.Setup(x => x.ReadPlayerIdentityByRoute(Request.Object.Path)).ReturnsAsync(identity);
_playerDataSource.Setup(x => x.ReadPlayerByRoute(identity.Player.PlayerRoute, null)).ReturnsAsync(identity.Player);
_authorizationPolicy.Setup(x => x.IsAuthorized(identity.Team)).Returns(Task.FromResult(new Dictionary<AuthorizedAction, bool> { { AuthorizedAction.EditTeam, true } }));
return identity;
}

[Fact]
public async Task Route_not_matching_identity_returns_404()
{
_playerDataSource.Setup(x => x.ReadPlayerIdentityByRoute(Request.Object.Path)).Returns(Task.FromResult<PlayerIdentity?>(null));

using (var controller = CreateController())
{
var result = await controller.Index();

Assert.IsType<NotFoundResult>(result);
}
}

[Fact]
public async Task Route_matching_identity_sets_authorization()
{
_ = SetupMocks();

using (var controller = CreateController())
{
var result = await controller.Index();

var model = (LinkedPlayersViewModel)((ViewResult)result).Model;

Assert.True(model.Authorization.CurrentMemberIsAuthorized[AuthorizedAction.EditTeam]);
}
}

[Fact]
public async Task Route_matching_identity_returns_player_and_identity()
{
var identity = SetupMocks();

using (var controller = CreateController())
{
var result = await controller.Index();

var model = (LinkedPlayersViewModel)((ViewResult)result).Model;

Assert.Equal(identity, model.ContextIdentity);
Assert.Equal(identity.Player, model.Player);
}
}

[Fact]
public async Task Route_matching_identity_sets_page_title()
{
var identity = SetupMocks();

using (var controller = CreateController())
{
var result = await controller.Index();

var model = (LinkedPlayersViewModel)((ViewResult)result).Model;

Assert.Equal($"Players linked to the statistics for {identity.PlayerIdentityName}", model.Metadata.PageTitle);
}
}

[Fact]
public async Task Route_matching_identity_sets_breadcrumbs()
{
var identity = SetupMocks();

using (var controller = CreateController())
{
var result = await controller.Index();

var model = (LinkedPlayersViewModel)((ViewResult)result).Model;

_breadcrumbBuilder.Verify(x => x.BuildBreadcrumbsForEditPlayers(model.Breadcrumbs, identity.Team!), Times.Once());
}
}
}
}
1 change: 1 addition & 0 deletions Stoolball.Web/Routing/StoolballRouteParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ public class StoolballRouteParser : IStoolballRouteParser
{ $"teams{SLASH}{ANY_VALID_ROUTE}{SLASH}edit{SLASH}players{OPTIONAL_SLASH}", StoolballRouteType.EditPlayersForTeam },
{ $"teams{SLASH}{ANY_VALID_ROUTE}{SLASH}edit{SLASH}players{SLASH}{ANY_VALID_ROUTE}{OPTIONAL_SLASH}", StoolballRouteType.PlayerIdentityActions },
{ $"teams{SLASH}{ANY_VALID_ROUTE}{SLASH}edit{SLASH}players{SLASH}{ANY_VALID_ROUTE}{SLASH}rename{OPTIONAL_SLASH}", StoolballRouteType.RenamePlayerIdentity },
{ $"teams{SLASH}{ANY_VALID_ROUTE}{SLASH}edit{SLASH}players{SLASH}{ANY_VALID_ROUTE}{SLASH}statistics{OPTIONAL_SLASH}", StoolballRouteType.LinkedPlayersForIdentity },
{ $"teams{SLASH}{ANY_VALID_ROUTE}{SLASH}delete{OPTIONAL_SLASH}", StoolballRouteType.DeleteTeam },
{ $"locations{SLASH}{ANY_VALID_ROUTE}{SLASH}matches{OPTIONAL_SLASH}", StoolballRouteType.MatchesForMatchLocation },
{ $"locations{SLASH}{ANY_VALID_ROUTE}{SLASH}matches{SLASH}ics{OPTIONAL_SLASH}", StoolballRouteType.MatchesCalendar },
Expand Down
1 change: 1 addition & 0 deletions Stoolball.Web/Routing/StoolballRouteType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public enum StoolballRouteType
PlayerBowling,
PlayerFielding,
LinkPlayerToMember,
LinkedPlayersForIdentity,
LinkedPlayersForMember,
RenamePlayerIdentity,
PlayersForTeam,
Expand Down
1 change: 1 addition & 0 deletions Stoolball.Web/Routing/StoolballRouteTypeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public class StoolballRouteTypeMapper : IStoolballRouteTypeMapper
{ StoolballRouteType.PlayerBowling, typeof(PlayerBowlingController) },
{ StoolballRouteType.PlayerFielding, typeof(PlayerFieldingController) },
{ StoolballRouteType.LinkPlayerToMember, typeof(LinkPlayerToMemberController) },
{ StoolballRouteType.LinkedPlayersForIdentity, typeof(LinkedPlayersForIdentityController) },
{ StoolballRouteType.LinkedPlayersForMember, typeof(LinkedPlayersForMemberController) },
{ StoolballRouteType.RenamePlayerIdentity, typeof(RenamePlayerIdentityController) },
{ StoolballRouteType.PlayerIdentityActions, typeof(PlayerIdentityActionsController) },
Expand Down
1 change: 1 addition & 0 deletions Stoolball.Web/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddTransient<PlayerBowlingController>();
services.AddTransient<PlayerFieldingController>();
services.AddTransient<LinkPlayerToMemberController>();
services.AddTransient<LinkedPlayersForIdentityController>();
services.AddTransient<LinkedPlayersForMemberController>();
services.AddTransient<PlayerIdentityActionsController>();
services.AddTransient<RenamePlayerIdentityController>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class LinkedPlayersForMemberController : RenderController, IRenderControl
[ContentSecurityPolicy]
public async new Task<IActionResult> Index()
{
var model = new LinkedPlayersForMemberViewModel(CurrentPage);
var model = new LinkedPlayersViewModel(CurrentPage);

var member = await _memberManager.GetCurrentMemberAsync();
if (member != null)
Expand Down
18 changes: 0 additions & 18 deletions Stoolball.Web/Statistics/Models/LinkedPlayersForMemberViewModel.cs

This file was deleted.

27 changes: 27 additions & 0 deletions Stoolball.Web/Statistics/Models/LinkedPlayersViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Stoolball.Statistics;
using Stoolball.Web.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;

namespace Stoolball.Web.Statistics.Models
{
public class LinkedPlayersViewModel : BaseViewModel
{
public LinkedPlayersViewModel(IPublishedContent? contentModel = null, IUserService? userService = null) : base(contentModel, userService)
{
}

public Player? Player { get; set; }

public PlayerIdentity? ContextIdentity { get; set; }

public string PreferredNextRoute { get; set; } = Constants.Pages.AccountUrl;

public string LinkedByHeading { get; set; } = "Linked by";
public string LinkedByMemberLabel { get; set; } = PlayerIdentityLinkedBy.Member.ToString();
public string LinkedByClubOrTeamLabel { get; set; } = PlayerIdentityLinkedBy.ClubOrTeam.ToString();
public string LinkedByStoolballEnglandLabel { get; set; } = PlayerIdentityLinkedBy.StoolballEngland.ToString();

public bool CanUnlinkIdentitiesLinkedByMember { get; set; }
}
}
64 changes: 64 additions & 0 deletions Stoolball.Web/Teams/LinkedPlayersForIdentityController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.Extensions.Logging;
using Stoolball.Data.Abstractions;
using Stoolball.Security;
using Stoolball.Teams;
using Stoolball.Web.Navigation;
using Stoolball.Web.Routing;
using Stoolball.Web.Security;
using Stoolball.Web.Statistics.Models;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Web.Common.Controllers;

namespace Stoolball.Web.Teams
{
public class LinkedPlayersForIdentityController : RenderController, IRenderControllerAsync
{
private readonly IAuthorizationPolicy<Team> _authorizationPolicy;
private readonly IPlayerDataSource _playerDataSource;
private readonly ITeamBreadcrumbBuilder _breadcrumbBuilder;

public LinkedPlayersForIdentityController(ILogger<LinkedPlayersForIdentityController> logger,
ICompositeViewEngine compositeViewEngine,
IUmbracoContextAccessor umbracoContextAccessor,
IAuthorizationPolicy<Team> authorizationPolicy,
IPlayerDataSource playerDataSource,
ITeamBreadcrumbBuilder breadcrumbBuilder)
: base(logger, compositeViewEngine, umbracoContextAccessor)
{
_authorizationPolicy = authorizationPolicy ?? throw new ArgumentNullException(nameof(authorizationPolicy));
_playerDataSource = playerDataSource ?? throw new ArgumentNullException(nameof(playerDataSource));
_breadcrumbBuilder = breadcrumbBuilder ?? throw new ArgumentNullException(nameof(breadcrumbBuilder));
}

[HttpGet]
[ContentSecurityPolicy]
public async new Task<IActionResult> Index()
{
var model = new LinkedPlayersViewModel(CurrentPage)
{
ContextIdentity = await _playerDataSource.ReadPlayerIdentityByRoute(Request.Path)
};

if (model.ContextIdentity?.Team == null || model.ContextIdentity?.Player == null)
{
return NotFound();
}
else
{
model.Player = await _playerDataSource.ReadPlayerByRoute(model.ContextIdentity.Player!.PlayerRoute!);

model.Authorization.CurrentMemberIsAuthorized = await _authorizationPolicy.IsAuthorized(model.ContextIdentity.Team);

model.Metadata.PageTitle = "Players linked to the statistics for " + model.ContextIdentity.PlayerIdentityName;

_breadcrumbBuilder.BuildBreadcrumbsForEditPlayers(model.Breadcrumbs, model.ContextIdentity.Team);

return CurrentTemplate(model);
}
}
}
}
1 change: 0 additions & 1 deletion Stoolball.Web/Teams/PlayerIdentityActionsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ public async new Task<IActionResult> Index()
}
else
{

model.Authorization.CurrentMemberIsAuthorized = await _authorizationPolicy.IsAuthorized(model.PlayerIdentity.Team);

model.Metadata.PageTitle = "Edit " + model.PlayerIdentity.PlayerIdentityName;
Expand Down

0 comments on commit c2b79ea

Please sign in to comment.