diff --git a/AuthPermissions/AuthPermissions.csproj b/AuthPermissions/AuthPermissions.csproj index cc518724..3a230cf5 100644 --- a/AuthPermissions/AuthPermissions.csproj +++ b/AuthPermissions/AuthPermissions.csproj @@ -22,4 +22,8 @@ + + + + diff --git a/AuthPermissions/AuthPermissionsOptions.cs b/AuthPermissions/AuthPermissionsOptions.cs index 846f8890..b8b275d8 100644 --- a/AuthPermissions/AuthPermissionsOptions.cs +++ b/AuthPermissions/AuthPermissionsOptions.cs @@ -4,37 +4,60 @@ using System; using System.Collections.Generic; using AuthPermissions.SetupParts; +using AuthPermissions.TenantParts; namespace AuthPermissions { public class AuthPermissionsOptions : IAuthPermissionsOptions { + /// + /// The different database types that AuthPermissions supports + /// public enum DatabaseTypes { NotSet, InMemory, SqlServer } + + //-------------------------------------------------- + //Tenant settings + + /// + /// This defines whether tenant code is activated, and whether the + /// multi-tenant is is a single layer, or many layers (hierarchical) + /// + public TenantTypes TenantType { get; set; } + + /// + /// If true, then the login must ask the user to pick which one they want to access + /// Also, the role admin allows you to set up different roles for each tenant the user is in + /// + public bool UserCanBeInMoreThanOneTenant { get; set; } + //------------------------------------------------- //internal set properties/handles + /// + /// Internal: holds the type of the Enum Permissions + /// public Type EnumPermissionsType { get; internal set; } /// - /// This contains the type of database used + /// Internal: This contains the type of database used /// public DatabaseTypes DatabaseType { get; internal set; } /// - /// This holds the a string containing the definition of the tenants + /// Internal: This holds the a string containing the definition of the tenants /// See the method for the format of the lines /// public string UserTenantSetupText { get; internal set; } /// - /// This holds the a string containing the definition of the RolesToPermission database class + /// Internal: This holds the a string containing the definition of the RolesToPermission database class /// See the method for the format of the lines /// public string RolesPermissionsSetupText { get; internal set; } /// - /// This holds the definition for a user, with its various parts + /// Internal: This holds the definition for a user, with its various parts /// See the class for information you need to provide /// public List UserRolesSetupData { get; internal set; } diff --git a/AuthPermissions/AuthSetupData.cs b/AuthPermissions/AuthSetupData.cs index cb6e2a0a..8c9623e5 100644 --- a/AuthPermissions/AuthSetupData.cs +++ b/AuthPermissions/AuthSetupData.cs @@ -1,25 +1,31 @@ // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ // Licensed under MIT license. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; -using AuthPermissions.SetupParts; using Microsoft.Extensions.DependencyInjection; namespace AuthPermissions { + /// + /// This class carries data through the setup extensions + /// public class AuthSetupData { - public AuthSetupData(IServiceCollection services, AuthPermissionsOptions options) + internal AuthSetupData(IServiceCollection services, AuthPermissionsOptions options) { Services = services; Options = options; } + /// + /// The DI ServiceCollection which AuthPermissions services, constants and policies are registered to + /// public IServiceCollection Services { get; } + /// + /// This holds the AuthPermissions options + /// public AuthPermissionsOptions Options { get; } } } \ No newline at end of file diff --git a/AuthPermissions/DataLayer/Classes/SupportTypes/AuthDbConstants.cs b/AuthPermissions/DataLayer/Classes/SupportTypes/AuthDbConstants.cs index eb71d0a9..7c82456f 100644 --- a/AuthPermissions/DataLayer/Classes/SupportTypes/AuthDbConstants.cs +++ b/AuthPermissions/DataLayer/Classes/SupportTypes/AuthDbConstants.cs @@ -13,6 +13,8 @@ public static class AuthDbConstants public const int TenantNameSize = 100; + public const int TenantDataKeySize = 100; + public const int DataKeySize = 64; } } \ No newline at end of file diff --git a/AuthPermissions/DataLayer/Classes/SupportTypes/TenantBase.cs b/AuthPermissions/DataLayer/Classes/SupportTypes/TenantBase.cs index a3ca171a..855df947 100644 --- a/AuthPermissions/DataLayer/Classes/SupportTypes/TenantBase.cs +++ b/AuthPermissions/DataLayer/Classes/SupportTypes/TenantBase.cs @@ -7,11 +7,6 @@ namespace AuthPermissions.DataLayer.Classes.SupportTypes { public abstract class TenantBase { - public TenantBase(Guid tenantId) - { - TenantId = tenantId; - } - - public Guid TenantId { get; private set; } + public int TenantId { get; protected set; } } } \ No newline at end of file diff --git a/AuthPermissions/DataLayer/Classes/Tenant.cs b/AuthPermissions/DataLayer/Classes/Tenant.cs new file mode 100644 index 00000000..eedaaa7a --- /dev/null +++ b/AuthPermissions/DataLayer/Classes/Tenant.cs @@ -0,0 +1,109 @@ +// Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ +// Licensed under MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using AuthPermissions.DataLayer.Classes.SupportTypes; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; + +namespace AuthPermissions.DataLayer.Classes +{ + /// + /// This is used for multi-tenant systems + /// + [Index(nameof(TenantName), IsUnique = true)] + public class Tenant :TenantBase + { + private HashSet _children; + private string _parentDataKey; + + /// + /// This defines a tenant in a single tenant multi-tenant system. + /// + /// + public Tenant(string tenantName) + { + TenantName = tenantName ?? throw new ArgumentNullException(nameof(tenantName)); + } + + private Tenant(string tenantName, string parentDataKey, Tenant parent) + { + TenantName = tenantName ?? throw new ArgumentNullException(nameof(tenantName)); + _parentDataKey = parentDataKey; + Parent = parent; + } + + + /// + /// This defines a tenant in a hierarchical multi-tenant system with a parent/child relationships + /// You MUST a) have every parent layer loaded and b) all parents must have a valid primary key + /// + public static Tenant SetupHierarchicalTenant(string tenantName, Tenant parent) + { + //We check that the higher layer a) are there and b) have a + + var lookParent = parent; + while (lookParent != null) + { + if (lookParent.TenantId == default) + throw new InvalidOperationException( + "The parent in the hierarchical setup doesn't have a valid primary key"); + if (lookParent.Parent == null && lookParent.ParentTenantId != null) + throw new InvalidOperationException( + "There is a parent layer that hasn't been read in"); + lookParent = lookParent.Parent; + } + + return new Tenant(tenantName, parent?.TenantDataKey, parent); + } + + /// + /// Easy way to see the tenant and its key + /// + /// + public override string ToString() + { + return $"{TenantName}: Key = {TenantDataKey}"; + } + + /// + /// This is the name defined for this tenant. This is unique + /// + [Required(AllowEmptyStrings = false)] + [MaxLength(AuthDbConstants.TenantNameSize)] + public string TenantName { get; private set; } + + /// + /// This contains the data key for this tenant. + /// If it is a single layer multi-tenant it will by the TenantId as a string + /// If it is a hierarchical multi-tenant it will contains a concatenation of the tenantsId + /// + [Required(AllowEmptyStrings = false)] + [MaxLength(AuthDbConstants.TenantDataKeySize)] + public string TenantDataKey => _parentDataKey + $".{TenantId}"; + + //--------------------------------------------------------- + //relationships - only used for hierarchical multi-tenant system + + /// + /// Foreign key to parent - can by null + /// + public int? ParentTenantId { get; private set; } + + /// + /// The parent tenant (if it exists) + /// + [ForeignKey(nameof(ParentTenantId))] + public Tenant Parent { get; private set; } + + /// + /// The optional children + /// + public IReadOnlyCollection Children => _children?.ToList(); + + } +} \ No newline at end of file diff --git a/AuthPermissions/DataLayer/Classes/TenantDefinition.cs b/AuthPermissions/DataLayer/Classes/TenantDefinition.cs deleted file mode 100644 index ac27b71c..00000000 --- a/AuthPermissions/DataLayer/Classes/TenantDefinition.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ -// Licensed under MIT license. See License.txt in the project root for license information. - -using System; -using System.ComponentModel.DataAnnotations; -using AuthPermissions.DataLayer.Classes.SupportTypes; - -namespace AuthPermissions.DataLayer.Classes -{ - public class TenantDefinition :TenantBase - { - public TenantDefinition(Guid tenantId, string tenantName) - : base(tenantId) - { - - } - - [Required(AllowEmptyStrings = false)] - [MaxLength(AuthDbConstants.TenantNameSize)] - public string TenantName { get; private set; } - } -} \ No newline at end of file diff --git a/AuthPermissions/DataLayer/Classes/UserDataKey.cs b/AuthPermissions/DataLayer/Classes/UserDataKey.cs index 8396ce05..41670745 100644 --- a/AuthPermissions/DataLayer/Classes/UserDataKey.cs +++ b/AuthPermissions/DataLayer/Classes/UserDataKey.cs @@ -9,11 +9,11 @@ namespace AuthPermissions.DataLayer.Classes { public class UserDataKey : TenantBase { - public UserDataKey(string userId, string dataKey, Guid tenantId = default) - : base(tenantId) + public UserDataKey(string userId, string dataKey, int tenantId = default) { UserId = userId ?? throw new ArgumentNullException(nameof(userId)); DataKey = dataKey ?? throw new ArgumentNullException(nameof(dataKey)); + TenantId = tenantId; } [Required(AllowEmptyStrings = false)] diff --git a/AuthPermissions/DataLayer/Classes/UserToRole.cs b/AuthPermissions/DataLayer/Classes/UserToRole.cs index e5d22694..d33b947f 100644 --- a/AuthPermissions/DataLayer/Classes/UserToRole.cs +++ b/AuthPermissions/DataLayer/Classes/UserToRole.cs @@ -15,15 +15,14 @@ namespace AuthPermissions.DataLayer.Classes /// public class UserToRole : TenantBase { - private UserToRole(Guid tenantId) //Needed by EF Core - : base(tenantId) {} + private UserToRole() {} //Needed by EF Core - public UserToRole(string userId, string userName, RoleToPermissions role, Guid tenantId = default) - : base(tenantId) + public UserToRole(string userId, string userName, RoleToPermissions role, int tenantId = default) { UserId = userId; UserName = userName; Role = role; + TenantId = tenantId; } //I use a composite key for this table: combination of UserId, TenantId and RoleName @@ -54,7 +53,7 @@ public override string ToString() public static IStatusGeneric AddRoleToUser(string userId, string userName, string roleName, - AuthPermissionsDbContext context, Guid tenantId = default) + AuthPermissionsDbContext context, int tenantId = default) { if (roleName == null) throw new ArgumentNullException(nameof(roleName)); diff --git a/AuthPermissions/DataLayer/EfCode/AuthPermissionsDbContext.cs b/AuthPermissions/DataLayer/EfCode/AuthPermissionsDbContext.cs index d8c6bda5..b8adc24e 100644 --- a/AuthPermissions/DataLayer/EfCode/AuthPermissionsDbContext.cs +++ b/AuthPermissions/DataLayer/EfCode/AuthPermissionsDbContext.cs @@ -13,13 +13,19 @@ public AuthPermissionsDbContext(DbContextOptions optio { } public DbSet RoleToPermissions { get; set; } - public DbSet UserTenantKey { get; set; } + public DbSet UserDataKey { get; set; } + public DbSet Tenants { get; set; } public DbSet UserToRoles { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema("authp"); + modelBuilder.Entity().HasKey(x => x.TenantId); + modelBuilder.Entity() + .Property("_parentDataKey") + .HasColumnName("ParentDataKey"); + modelBuilder.Entity() .HasKey(x => new { x.UserId, x.TenantId, x.RoleName }); diff --git a/AuthPermissions/SetupExtensions.cs b/AuthPermissions/SetupExtensions.cs index 73657a14..edbbb9ed 100644 --- a/AuthPermissions/SetupExtensions.cs +++ b/AuthPermissions/SetupExtensions.cs @@ -76,10 +76,22 @@ public static AuthSetupData UsingInMemoryDatabase(this AuthSetupData setupData) return setupData; } - //public static AuthSetupData AddTenantsIfEmpty(this AuthSetupData setupData, string linesOfText) - //{ - // return setupData; - //} + /// + /// This allows you to define the name of each tenant by name + /// If you are using a hierarchical tenant design, then the whole + /// + /// + /// If you are using a single layer then each line contains the a tenant name + /// If you are using hierarchical tenant, then each line contains the whole hierarchy with '|' as separator, e.g. + /// Holding company | USA branch | East Coast | New York + /// Holding company | USA branch | East Coast | Washington + /// + /// + public static AuthSetupData AddTenantsIfEmpty(this AuthSetupData setupData, string linesOfText) + { + setupData.Options.UserTenantSetupText = linesOfText ?? throw new ArgumentNullException(nameof(linesOfText)); + return setupData; + } /// /// This allows you to add Roles with their permissions, but only if the auth database contains NO RoleToPermissions @@ -93,13 +105,13 @@ public static AuthSetupData UsingInMemoryDatabase(this AuthSetupData setupData) /// AuthSetupData public static AuthSetupData AddRolesPermissionsIfEmpty(this AuthSetupData setupData, string linesOfText) { - setupData.Options.RolesPermissionsSetupText = linesOfText; + setupData.Options.RolesPermissionsSetupText = linesOfText ?? throw new ArgumentNullException(nameof(linesOfText)); return setupData; } /// /// This allows you to add what roles a user has, but only if the auth database doesn't have any UserToRoles in the database - /// NOTE: The parameter must contain a list of userId+roles. + /// The parameter must contain a list of userId+roles. /// /// /// A list of containing the information on users and what auth roles they have. diff --git a/AuthPermissions/SetupParts/Internal/CommaDelimitedHandler.cs b/AuthPermissions/SetupParts/Internal/CommaDelimitedHandler.cs index 9094b1d3..ad32185f 100644 --- a/AuthPermissions/SetupParts/Internal/CommaDelimitedHandler.cs +++ b/AuthPermissions/SetupParts/Internal/CommaDelimitedHandler.cs @@ -8,12 +8,17 @@ namespace AuthPermissions.SetupParts.Internal { internal static class CommaDelimitedHandler { - public static List DecodeCheckCommaDelimitedString(this string line, int charNum, Action checkValid) + public static List DecodeCodeNameWithTrimming(this string line, int charNum, Action checkValid) { var trimmedNames = new List(); while (charNum < line.Length) { - if (!char.IsLetterOrDigit(line[charNum])) charNum++; + if (!char.IsLetterOrDigit(line[charNum])) + { + charNum++; + continue; + } + var foundName = ""; var startOfName = charNum; while (charNum < line.Length && char.IsLetterOrDigit(line[charNum])) diff --git a/AuthPermissions/SetupParts/Internal/SetupRolesService.cs b/AuthPermissions/SetupParts/Internal/SetupRolesService.cs index 42102458..55f65c04 100644 --- a/AuthPermissions/SetupParts/Internal/SetupRolesService.cs +++ b/AuthPermissions/SetupParts/Internal/SetupRolesService.cs @@ -35,14 +35,18 @@ public IStatusGeneric AddRolesToDatabaseIfEmpty(string linesOfText, Type enumPer return status; } - //https://stackoverflow.com/questions/45758587/how-can-i-turn-a-multi-line-string-into-an-array-where-each-element-is-a-line-of - var lines = linesOfText.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + var lines = linesOfText.Split( Environment.NewLine); for (int i = 0; i < lines.Length; i++) { + if (string.IsNullOrWhiteSpace(lines[i])) + continue; status.CombineStatuses(DecodeLineAndAddToDb(lines[i], i, enumPermissionType)); } + if (status.IsValid) + _context.SaveChanges(); + status.Message = $"Added {lines.Length} new RoleToPermissions to the auth database"; //If there is an error this message is removed return status; } @@ -81,7 +85,7 @@ private IStatusGeneric DecodeLineAndAddToDb(string line, int lineNum, Type enumP charNum = indexColon + 2; } - var validPermissionNames = line.DecodeCheckCommaDelimitedString(charNum, + var validPermissionNames = line.DecodeCodeNameWithTrimming(charNum, (name, startOfName) => { if (!enumPermissionType.PermissionsNameIsValid(name)) diff --git a/AuthPermissions/SetupParts/Internal/SetupTenantsService.cs b/AuthPermissions/SetupParts/Internal/SetupTenantsService.cs new file mode 100644 index 00000000..e7e5ba8d --- /dev/null +++ b/AuthPermissions/SetupParts/Internal/SetupTenantsService.cs @@ -0,0 +1,175 @@ +// Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ +// Licensed under MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using AuthPermissions.DataLayer.Classes; +using AuthPermissions.DataLayer.EfCode; +using AuthPermissions.TenantParts; +using Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; +using Microsoft.EntityFrameworkCore.ValueGeneration; +using StatusGeneric; + +namespace AuthPermissions.SetupParts.Internal +{ + internal class SetupTenantsService + { + private readonly AuthPermissionsDbContext _context; + + public SetupTenantsService(AuthPermissionsDbContext context) + { + _context = context; + } + + public async Task AddTenantsToDatabaseIfEmptyAsync(string linesOfText, AuthPermissionsOptions options) + { + var status = new StatusGenericHandler(); + + //Check the options are set + if (options.TenantType == TenantTypes.NotUsingTenants) + return status.AddError( + $"You must set the options {nameof(AuthPermissionsOptions.TenantType)} to allow tenants to be processed"); + + if (_context.Tenants.Any()) + { + status.Message = + "There were already Tenants in the auth database, so didn't add these tenants"; + return status; + } + + var lines = linesOfText.Split( Environment.NewLine); + + //Check for duplicate tenant names + var dups = lines.GroupBy(line => line).Where(name => name.Count() > 1).ToList(); + if (dups.Any()) + return status.AddError("There were tenants with duplicate names, they are: " + string.Join(Environment.NewLine, dups.Select(x => x.Key))); + + if (options.TenantType == TenantTypes.SingleTenant) + { + foreach (var line in lines) + { + _context.Add(new Tenant(line.Trim())); + } + + await _context.SaveChangesAsync(); + } + else //hierarchical + { + //This decodes the hierarchical tenants + var entries = new List(); + for (int i = 0; i < lines.Length; i++) + { + if (string.IsNullOrWhiteSpace(lines[i])) + continue; + entries.Add(new TenantNameDecoded(lines[i], i)); + } + + var tenantLookup = new Dictionary(); + //This creates a group with the higher levels first + var groupByLayers = entries.GroupBy(x => x.TenantNamesInOrder.Count); + + //This uses a transactions because its going to be calling SaveChanges for each layer + using(var transaction = await _context.Database.BeginTransactionAsync()) + { + //This will save a layer, so that the next layer down can be saved + foreach (var groupByLayer in groupByLayers) + { + var tenantsToAddToDb = new List(); + foreach (var tenantNameDecoded in groupByLayer) + { + Tenant parent = null; + if (tenantNameDecoded.ParentFullName != null) + { + if (!tenantLookup.TryGetValue(tenantNameDecoded.ParentFullName, out parent)) + status.AddError( + $"The tenant {tenantNameDecoded.TenantFullName} on line {tenantNameDecoded.LineNum} parent {tenantNameDecoded.ParentFullName} was not found"); + } + + if (tenantLookup.ContainsKey(tenantNameDecoded.TenantFullName)) + status.AddError( + $"The tenant {tenantNameDecoded.TenantFullName} on line {tenantNameDecoded.LineNum} is a duplicate of the same name defined earlier"); + var newTenant = Tenant.SetupHierarchicalTenant(tenantNameDecoded.TenantFullName, parent); + tenantsToAddToDb.Add(newTenant); + tenantLookup[tenantNameDecoded.TenantFullName] = newTenant; + } + if (status.IsValid) + { + //we add all the tenants in this layer + _context.AddRange(tenantsToAddToDb); + await _context.SaveChangesAsync(); + } + } + if (status.IsValid) + transaction.Commit(); + } + } + + return status; + } + + //----------------------------------------------------------- + //private parts + + private class TenantNameDecoded + { + public TenantNameDecoded(string line, int lineNum) + { + DecodeNamesDelimitedBy(line, '|'); + LineNum = lineNum; + + if (!TenantNamesInOrder.Any()) + throw new InvalidOperationException($"line {lineNum} produced no tenant names"); + + ParentFullName = TenantNamesInOrder.Count > 1 + ? CombineName(TenantNamesInOrder.Take(TenantNamesInOrder.Count - 1)) + : (string)null; + TenantFullName = CombineName(TenantNamesInOrder); + } + + public List TenantNamesInOrder { get; } = new List(); + public List TenantNameStartCharNum { get; } = new List(); + + public int LineNum { get; } + + public string ParentFullName { get; } + public string TenantFullName { get; } + + private string CombineName(IEnumerable names) + { + return string.Join(" | ", names); + } + + private void DecodeNamesDelimitedBy(string line, char delimiterChar) + { + var charNum = 0; + while (charNum < line.Length) + { + if (line[charNum] == ' ') + { + charNum++; + continue; + } + + var foundName = ""; + var startOfName = charNum; + while (charNum < line.Length && line[charNum] != delimiterChar) + { + foundName += line[charNum]; + charNum++; + } + if (foundName.Length > 0) + { + TenantNamesInOrder.Add(foundName.TrimEnd()); + TenantNameStartCharNum.Add( startOfName); + } + charNum++; + } + } + } + + } +} \ No newline at end of file diff --git a/AuthPermissions/SetupParts/Internal/SetupUsersService.cs b/AuthPermissions/SetupParts/Internal/SetupUsersService.cs index be19e28d..cd73fa9a 100644 --- a/AuthPermissions/SetupParts/Internal/SetupUsersService.cs +++ b/AuthPermissions/SetupParts/Internal/SetupUsersService.cs @@ -55,7 +55,7 @@ private async Task CreateUserTenantAndAddToDbAsync(DefineUserWit var status = new StatusGenericHandler(); var rolesToPermissions = new List(); - userDefine.RoleNamesCommaDelimited.DecodeCheckCommaDelimitedString(0, + userDefine.RoleNamesCommaDelimited.DecodeCodeNameWithTrimming(0, (name, startOfName) => { var roleToPermission = _context.RoleToPermissions.SingleOrDefault(x => x.RoleName == name); diff --git a/AuthPermissions/TenantParts/TenantTypes.cs b/AuthPermissions/TenantParts/TenantTypes.cs new file mode 100644 index 00000000..bce2c4c7 --- /dev/null +++ b/AuthPermissions/TenantParts/TenantTypes.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ +// Licensed under MIT license. See License.txt in the project root for license information. + +namespace AuthPermissions.TenantParts +{ + /// + /// This defines the types of tenant the AuthPermissions can handle + /// + public enum TenantTypes + { + /// + /// Usage of tenants are turned off + /// + NotUsingTenants, + /// + /// Multi-tenant with one level only, e.g. a company has different departments: sales, finance, HR etc. + /// A User can only be in one of these levels + /// + SingleTenant, + /// + /// Multi-tenant many levels, e.g. Holding company -> USA branch -> East Coast -> New York + /// A User at the USA branch has read/write access to the USA branch data, read-only access to the East Coast and all its subsidiaries + /// + HierarchicalTenant + } +} \ No newline at end of file diff --git a/Example1.RazorPages.IndividualAccounts/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/Example1.RazorPages.IndividualAccounts/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs index 053eac89..31c25814 100644 --- a/Example1.RazorPages.IndividualAccounts/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs +++ b/Example1.RazorPages.IndividualAccounts/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs @@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using System; -using Example1.RazorPages.IndividualAccounts.Data; namespace Example1.RazorPages.IndividualAccounts.Data.Migrations { diff --git a/Example1.RazorPages.IndividualAccounts/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/Example1.RazorPages.IndividualAccounts/Data/Migrations/ApplicationDbContextModelSnapshot.cs index 47215a2b..c39e7dc2 100644 --- a/Example1.RazorPages.IndividualAccounts/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Example1.RazorPages.IndividualAccounts/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -5,7 +5,6 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using System; -using Example1.RazorPages.IndividualAccounts.Data; namespace Example1.RazorPages.IndividualAccounts.Data.Migrations { diff --git a/Test/UnitTests/TestAuthPermissions/TestSetupPartsSetupTenantService.cs b/Test/UnitTests/TestAuthPermissions/TestSetupPartsSetupTenantService.cs new file mode 100644 index 00000000..7a73f588 --- /dev/null +++ b/Test/UnitTests/TestAuthPermissions/TestSetupPartsSetupTenantService.cs @@ -0,0 +1,171 @@ +// Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ +// Licensed under MIT license. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Runtime.InteropServices.ComTypes; +using System.Threading.Tasks; +using AuthPermissions; +using AuthPermissions.DataLayer.EfCode; +using AuthPermissions.SetupParts.Internal; +using AuthPermissions.TenantParts; +using TestSupport.EfHelpers; +using Xunit; +using Xunit.Abstractions; +using Xunit.Extensions.AssertExtensions; + +namespace Test.UnitTests.TestAuthPermissions +{ + public class TestSetupPartsSetupTenantService + { + private readonly ITestOutputHelper _output; + + public TestSetupPartsSetupTenantService(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public async Task TestAddTenantsToDatabaseIfEmptyBadOptions() + { + //SETUP + var options = SqliteInMemory.CreateOptions(); + using var context = new AuthPermissionsDbContext(options); + context.Database.EnsureCreated(); + + var service = new SetupTenantsService(context); + + //ATTEMPT + var status = await service.AddTenantsToDatabaseIfEmptyAsync("", new AuthPermissionsOptions()); + + //VERIFY + status.IsValid.ShouldBeFalse(); + status.GetAllErrors() + .ShouldEqual( + $"You must set the options {nameof(AuthPermissionsOptions.TenantType)} to allow tenants to be processed"); + } + + [Fact] + public async Task TestAddTenantsToDatabaseIfEmptySingleTenant() + { + //SETUP + var options = SqliteInMemory.CreateOptions(); + using var context = new AuthPermissionsDbContext(options); + context.Database.EnsureCreated(); + + var service = new SetupTenantsService(context); + var authOptions = new AuthPermissionsOptions + { + TenantType = TenantTypes.SingleTenant + }; + var lines = @"Tenant1 +Tenant2 +Tenant3"; + + //ATTEMPT + var status = await service.AddTenantsToDatabaseIfEmptyAsync(lines, authOptions); + + //VERIFY + status.IsValid.ShouldBeTrue(status.GetAllErrors()); + context.ChangeTracker.Clear(); + context.Tenants.Count().ShouldEqual(3); + } + + [Fact] + public async Task TestAddTenantsToDatabaseIfEmptySingleTenantDuplicate() + { + //SETUP + var options = SqliteInMemory.CreateOptions(); + using var context = new AuthPermissionsDbContext(options); + context.Database.EnsureCreated(); + + var service = new SetupTenantsService(context); + var authOptions = new AuthPermissionsOptions + { + TenantType = TenantTypes.SingleTenant + }; + var lines = @"Tenant1 +Tenant2 +Tenant2 +Tenant3 +Tenant3"; + + //ATTEMPT + var status = await service.AddTenantsToDatabaseIfEmptyAsync(lines, authOptions); + + //VERIFY + status.IsValid.ShouldBeFalse(status.GetAllErrors()); + status.GetAllErrors() + .ShouldEqual($"There were tenants with duplicate names, they are: Tenant2{Environment.NewLine}Tenant3"); + } + + [Fact] + public async Task TestAddTenantsToDatabaseIfEmptyHierarchicalTenant() + { + //SETUP + var options = SqliteInMemory.CreateOptions(); + using var context = new AuthPermissionsDbContext(options); + context.Database.EnsureCreated(); + + var service = new SetupTenantsService(context); + var authOptions = new AuthPermissionsOptions + { + TenantType = TenantTypes.HierarchicalTenant + }; + var lines = @"Company +Company | West Coast | +Company | West Coast | SanFran +Company | West Coast | SanFran | Shop1 +Company | West Coast | SanFran | Shop2 +Company | West Coast | LA +Company | West Coast | LA | Shop1 +Company | West Coast | LA | Shop2"; + + //ATTEMPT + var status = await service.AddTenantsToDatabaseIfEmptyAsync(lines, authOptions); + + //VERIFY + status.IsValid.ShouldBeTrue(status.GetAllErrors()); + context.ChangeTracker.Clear(); + var tenants = context.Tenants.ToList(); + foreach (var tenant in tenants) + { + _output.WriteLine(tenant.ToString()); + } + context.Tenants.Count().ShouldEqual(8); + } + + [Fact] + public async Task TestAddTenantsToDatabaseIfEmptyHierarchicalTenantBadName() + { + //SETUP + var options = SqliteInMemory.CreateOptions(); + using var context = new AuthPermissionsDbContext(options); + context.Database.EnsureCreated(); + + var service = new SetupTenantsService(context); + var authOptions = new AuthPermissionsOptions + { + TenantType = TenantTypes.HierarchicalTenant + }; + var lines = @"Company +Company | West Coast | +Company | West Coast | San??? +Company | XX Coast | SanFran | Shop1 +Company | West Coast | SanFran | Shop2 +Company | West Coast | LA +Company | YY Coast | LA | Shop1 +Company | West Coast | LA | Shop2"; + + //ATTEMPT + var status = await service.AddTenantsToDatabaseIfEmptyAsync(lines, authOptions); + + //VERIFY + status.IsValid.ShouldBeFalse(); + status.Errors.Count.ShouldEqual(3); + status.Errors[0].ToString().ShouldEqual("The tenant Company | XX Coast | SanFran | Shop1 on line 3 parent Company | XX Coast | SanFran was not found"); + status.Errors[1].ToString().ShouldEqual("The tenant Company | West Coast | SanFran | Shop2 on line 4 parent Company | West Coast | SanFran was not found"); + status.Errors[2].ToString().ShouldEqual("The tenant Company | YY Coast | LA | Shop1 on line 6 parent Company | YY Coast | LA was not found"); + } + } +} \ No newline at end of file