Know Your Roles! π§
When reviewing our Kentico 12 MVC applications we probably have some lines of code that look something like the following:
int userId = ...
if (userManager.IsInRole(userId, "Manager"))
{
IEnumerable<string> protectedData = GetTheData();
return protectedData;
}
else
{
return Enumerable.Empty<string>();
}
For this code to function correctly, when your site is running, the "Manager"
role must exist in the database and be assigned to the right users.
How do we know, at any given time, this is true? π
What happens when we make changes to roles in our code or the database (either adding, removing, or renaming them)? π
Is there a way to easily verify all the roles in our code match the roles defined in the CMS?
Avoid Scattered Strings
My first recommendation would be to avoid using "Scattered Strings".
How many places throughout our code do we have lines like these?
if (userManager.IsInRole(userId, "Manager")) { ... }
userManager.AddToRole(userId, "SpecialUser"));
userManager.RemoveFromRole(userId, "Admin"));
Those role names can quickly become scattered throughout our application, which makes them hard to keep track of and update. Typos can be easy to miss, even with code reviews. π£
These aren't Magic Strings because their purpose is clear - but it is a value that the compiler cannot help us with since string
values don't have a business meaning.
A typo'd string
is the same as a string
with the correct value as far as the compiler is concerned.
Primitive types can be a poor choice for storing values that have important business meaning. If you find yourself running into issues with Scattered Strings in your code base you might have a case of Primitive Obsession
We can instead replace them with a static Role class that contains all of our roles.
public static class UserRoles
{
public const string MANAGER = nameof(MANAGER);
public const string SPECIAL_USER = nameof(SPECIAL_USER);
public const string ADMIN = nameof(ADMIN);
}
Using the nameof() operator helps ensure the name of the constant and its value are never out of sync.
Now we can use this class wherever we'd normally use our role name strings. π
if (userManager.IsInRole(userId, UserRoles.MANAGER)) { ... }
Avoiding Scattered Strings helps reduce typos and makes changes easier by relying on the C# language and Visual Studio refactoring tools when adding, removing, or updating a role name. π
But, it doesn't prevent us from having a role defined in the code that doesn't exist in the database. π―
Write Integration Tests
The best way to check that the database always matches our code's expectations is to write, and then run, some integration tests.
If you haven't written Integration Tests for your Kentico sites before, checkout my post "Setting Up Integration Tests":
Kentico 12: Design Patterns Part 8 - Setting Up Integration Tests
Sean G. Wright γ» Jul 22 '19 γ» 10 min read
#dotnet #kentico #integrationtests #testing
So what might our integration test look like?
We could query the database for each one of our roles and make sure it's not null
:
[TestFixture]
public class UserRolesTests : IntegrationTests
{
private readonly int siteId = 1;
[Test]
public void UserRoles_Will_Be_In_The_Database()
{
RoleInfo managerRole = RoleInfoProvider
.GetRoleInfo(UserRoles.MANAGER, siteId);
RoleInfo specialUserRole = RoleInfoProvider
.GetRoleInfo(UserRoles.SPECIAL_USER, siteId);
RoleInfo adminRole = RoleInfoProvider
.GetRoleInfo(UserRoles.ADMIN, siteId);
managerRole.Should().NotBeNull();
specialUserRole.Should().NotBeNull();
adminRole.Should().NotBeNull();
}
}
I'm using FluentAssertions to give me access to the Behavior Driven Design (BDD) style
.Should()
extension methods.
This does the trick! π
If any of the roles we are querying are missing from the database, then our integration test will fail. π
However, it seems easy enough to accidentally leave out a role in the above test, especially if we have a large number of roles in our application. π£
That role querying and asserting also looks to be doing the same thing over and over.
Let's try again:
[TestFixture]
public class UserRolesTests : IntegrationTests
{
private readonly int siteId = 1;
[Test]
public void UserRoles_Will_Be_In_The_Database()
{
List<string> appRoleNames = typeof(UserRoles)
.GetFields(BindingFlags.Public | BindingFlags.Static)
.Select(f => f.Name)
.ToList();
List<string> roleNames = RoleInfoProvider.GetRoles()
.WhereEquals(nameof(RoleInfo.SiteID), siteId)
.WhereIn(nameof(RoleInfo.RoleName), userRoleNames)
.Column(nameof(RoleInfo.RoleName))
.AsEnumerable()
.Select(r => r.RoleName)
.ToList();
roleNames.Should().BeEquivalentTo(appRoleNames);
}
}
With this test we use reflection to gather up all the names of the fields of the UserRoles
class. π§
We use the RoleInfoProvider.GetRoles()
method, combined with the powerful ObjectQuery extension methods to retrieve the same set of roles from the database. πͺ
We then assert that the two collections of strings match.
Nice! π
Notice how we use the
nameof()
operator in ourObjectQuery
as well. I recommend you use it instead of typing out the string names of the database columns to avoid the same Scattered String issues we saw about with Roles.Since the convention in Kentico is to name class property names the same as database column names, the
nameof()
operator is a great way to avoid typos. π€
Summary
There's always going to be a disconnect between data sources and application code. Our applications are often designed to be stateless to allow scaling, while our data sources are where we store all the shared application state.
When we need to have logic in our application based on that state we encode values from the data source, like role names, into our code.
Integration tests are a great way to ensure that the expectations of our application match what the data source will give us. π
Thanks for reading!
If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:
Or my Kentico blog series:
Top comments (0)