commit 0dc4240d2cc578018822f7450c59df6d64cc6d79 Author: alexandre-spieser Date: Sun Oct 22 00:24:46 2017 +0000 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c99ff6a --- /dev/null +++ b/.gitignore @@ -0,0 +1,299 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Typescript v1 declaration files +typings/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs \ No newline at end of file diff --git a/AspNetCore.Identity.MongoDbCore.sln b/AspNetCore.Identity.MongoDbCore.sln new file mode 100644 index 0000000..86d3527 --- /dev/null +++ b/AspNetCore.Identity.MongoDbCore.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.16 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.Identity.MongoDbCore", "src\AspNetCore.Identity.MongoDbCore.csproj", "{C2EB6500-79A2-410E-8BF7-ACACE86C7A0E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{059DB57E-6EC2-4F6D-BE0F-343B00976DA9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7819C5A0-617F-488D-9791-2C4859B9BF19}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.Identity.MongoDbCore.IntegrationTests", "test\AspNetCore.Identity.MongoDbCore.IntegrationTests\AspNetCore.Identity.MongoDbCore.IntegrationTests.csproj", "{FA1D3288-90E8-4831-B533-A2B3D4F034B1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{EE14D20F-C6E9-4A4C-A13E-C1A90BCA8763}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C2EB6500-79A2-410E-8BF7-ACACE86C7A0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2EB6500-79A2-410E-8BF7-ACACE86C7A0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2EB6500-79A2-410E-8BF7-ACACE86C7A0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2EB6500-79A2-410E-8BF7-ACACE86C7A0E}.Release|Any CPU.Build.0 = Release|Any CPU + {FA1D3288-90E8-4831-B533-A2B3D4F034B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA1D3288-90E8-4831-B533-A2B3D4F034B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA1D3288-90E8-4831-B533-A2B3D4F034B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA1D3288-90E8-4831-B533-A2B3D4F034B1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C2EB6500-79A2-410E-8BF7-ACACE86C7A0E} = {059DB57E-6EC2-4F6D-BE0F-343B00976DA9} + {FA1D3288-90E8-4831-B533-A2B3D4F034B1} = {7819C5A0-617F-488D-9791-2C4859B9BF19} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B6A5D462-4085-442D-9E31-DD2CAD280D3C} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..8856988 --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# Microsoft.AspNetCore.Identity.MongoDbCore +A MongoDb UserStore and RoleStore adapter for Microsoft.AspNetCore.Identity 2.0. +Allows you to use MongoDb instead of SQL server with Microsoft.AspNetCore.Identity 2.0. +Covered by 730+ integration tests and unit tests from the modified Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test test suite. + +# Usage examples + +Your user and role entities must inherit from MongoIdentityUser and MongoIdentityRole in a way similar to the IdentityUser and the IdentityRole in Microsoft.AspNetCore.Identity. + +```csharp + + public class ApplicationRole : MongoIdentityRole + { + public ApplicationRole() : base() + { + } + + public ApplicationRole(string roleName) : base(roleName) + { + } + } + + public class ApplicationUser : MongoIdentityUser + { + public ApplicationUser() : base() + { + } + + public ApplicationUser(string userName, string email) : base(userName, email) + { + } + } + + /// + /// This method gets called by the runtime. Use this method to add services to the container. + /// + /// + public void ConfigureServices(IServiceCollection services) + { + var builder = new ConfigurationBuilder() + .SetBasePath(_hostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{_hostingEnvironment.EnvironmentName}.json", optional: true); + + + Configuration = builder.Build(); + + services.AddOptions(); + + // add a global config object + services.AddSingleton(Configuration); + var mongoSettings = Configuration.GetSection(nameof(MongoDbSettings)); + var settings = Configuration.GetSection(nameof(MongoDbSettings)).Get(); + var mongoDbIdentityConfiguration = new MongoDbIdentityConfiguration + { + MongoDbSettings = settings, + IdentityOptionsAction = options => + { + options.Password.RequireDigit = false; + options.Password.RequiredLength = 8; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + options.Password.RequireLowercase = false; + + // Lockout settings + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); + options.Lockout.MaxFailedAccessAttempts = 10; + + + // ApplicationUser settings + options.User.RequireUniqueEmail = true; + options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@.-_"; + } + }; + services.ConfigureMongoDbIdentity(mongoDbIdentityConfiguration); + } +``` + +## Author +**Alexandre Spieser** + +## Donations +Feeling like my work is worth a coffee? +Donations are welcome and will go towards further development of this project as well as other MongoDb related projects. Use the wallet address below to donate. +BTC Donations: 1Qc5ZpNA7g66KEEMcz7MXxwNyyoRyKJJZ + +*Thank you for your support and generosity!* + +## License +mongodb-generic-repository is under MIT license - http://www.opensource.org/licenses/mit-license.php + +The MIT License (MIT) + +Copyright (c) 2016-2017 Alexandre Spieser + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +## Copyright +Copyright © 2017 diff --git a/src/AspNetCore.Identity.MongoDbCore.csproj b/src/AspNetCore.Identity.MongoDbCore.csproj new file mode 100644 index 0000000..04aaa7c --- /dev/null +++ b/src/AspNetCore.Identity.MongoDbCore.csproj @@ -0,0 +1,39 @@ + + + + netcoreapp2.0 + + + + bin\Release\netcoreapp2.0\AspNetCore.Identity.MongoDbCore.xml + + + + bin\Debug\netcoreapp2.0\AspNetCore.Identity.MongoDbCore.xml + + + + + + + + + + + + + Resources.resx + True + True + + + + + + Resources.Designer.cs + ResXFileCodeGenerator + Always + + + + diff --git a/src/AspNetCore.Identity.MongoDbCore.nuspec b/src/AspNetCore.Identity.MongoDbCore.nuspec new file mode 100644 index 0000000..387aacc --- /dev/null +++ b/src/AspNetCore.Identity.MongoDbCore.nuspec @@ -0,0 +1,20 @@ + + + + AspNetCore.Identity.MongoDbCore + 1.0.3 + AspNetCore.Identity.MongoDbCore + Alexandre Spieser + Alexandre Spieser + http://www.opensource.org/licenses/mit-license.php + https://github.com/alexandre-spieser/AspNetCore.Identity.MongoDbCore + false + A MongoDb UserStore and RoleStore adapter for Microsoft.AspNetCore.Identity 2.0. + First release. + Copyright 2017 (c) Alexandre Spieser. All rights reserved. + aspnetcore mongo mongodb identity membership + + + + + \ No newline at end of file diff --git a/src/Extensions/ClaimHolderExtensions.cs b/src/Extensions/ClaimHolderExtensions.cs new file mode 100644 index 0000000..9decc35 --- /dev/null +++ b/src/Extensions/ClaimHolderExtensions.cs @@ -0,0 +1,122 @@ +using AspNetCore.Identity.MongoDbCore.Interfaces; +using AspNetCore.Identity.MongoDbCore.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; + +namespace AspNetCore.Identity.MongoDbCore.Extensions +{ + /// + /// The extensions for an object that holds claims. + /// + public static class ClaimHolderExtensions + { + public static MongoClaim ToMongoClaim(this Claim claim) + { + return new MongoClaim + { + Type = claim.Type, + Value = claim.Value, + Issuer = claim.Issuer + }; + } + + public static Claim ToClaim(this MongoClaim claim) + { + return new Claim(claim.Type, claim.Value, null, claim.Issuer); + } + + /// + /// Adds a claim to a claim holder, implementing . + /// + /// The object holding claims. + /// The claim you want to add. + /// Returns true if the claim was added. + public static bool AddClaim(this IClaimHolder claimHolder, Claim claim) + { + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + + // prevent adding duplicate claims + if (claimHolder.HasClaim(claim)) + { + return false; + } + + claimHolder.Claims.Add(claim.ToMongoClaim()); + return true; + } + + /// + /// Replaces a claim on a claim holder, implementing . + /// + /// The object holding claims. + /// The claim you want to replace. + /// The new claim you want to set. + /// Returns true if the claim was replaced. + public static bool ReplaceClaim(this IClaimHolder claimHolder, Claim claim, Claim newClaim) + { + var replaced = false; + claimHolder.Claims.Where(uc => uc.Value == claim.Value && uc.Type == claim.Type).ToList() + .ForEach(oldClaim => { + oldClaim.Type = newClaim.Type; + oldClaim.Value = newClaim.Value; + oldClaim.Issuer = newClaim.Issuer; + replaced |= true; + }); + return replaced; + } + + /// + /// Checks if an object implementing has a claim. + /// + /// The object holding claims. + /// The claim you want to replace. + /// Returns true if the claim is present, false otherwise. + public static bool HasClaim(this IClaimHolder claimHolder, Claim claim) + { + if(claimHolder.Claims == null) + { + claimHolder.Claims = new List(); + } + return claimHolder.Claims.Any(e => e.Value == claim.Value && e.Type == claim.Type); + } + + public static bool RemoveClaim(this IClaimHolder claimHolder, Claim claim) + { + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + var exists = claimHolder.Claims + .FirstOrDefault(e => e.Value == claim.Value + && e.Type == claim.Type); + if (exists != null) + { + claimHolder.Claims.Remove(exists); + return true; + } + return false; + } + + public static bool RemoveClaims(this IClaimHolder claimHolder, IEnumerable claims) + { + var someClaimsRemoved = false; + foreach (var claim in claims) + { + var matchedClaims = claimHolder.Claims.Where(uc => uc.Value == claim.Value && uc.Type == claim.Type) + .ToList(); + + foreach (var c in matchedClaims) + { + claimHolder.Claims.Remove(c); + someClaimsRemoved |= true; + } + } + return someClaimsRemoved; + } + } +} diff --git a/src/Extensions/ServiceCollectionExtension.cs b/src/Extensions/ServiceCollectionExtension.cs new file mode 100644 index 0000000..10b3a52 --- /dev/null +++ b/src/Extensions/ServiceCollectionExtension.cs @@ -0,0 +1,94 @@ +using AspNetCore.Identity.MongoDbCore.Infrastructure; +using AspNetCore.Identity.MongoDbCore.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using MongoDbGenericRepository; +using System; + +namespace AspNetCore.Identity.MongoDbCore.Extensions +{ + public class MongoDbSettings + { + public string ConnectionString { get; set; } + public string DatabaseName { get; set; } + } + + public class MongoDbIdentityConfiguration + { + public MongoDbSettings MongoDbSettings { get; set; } + public Action IdentityOptionsAction { get; set; } + } + + public static class ServiceCollectionExtension + { + public static void ConfigureMongoDbIdentity( + this IServiceCollection services, + MongoDbIdentityConfiguration mongoDbIdentityConfiguration, + IMongoRepository mongoRepository = null) + where TUser : MongoIdentityUser, new() + where TKey : IEquatable + { + services.AddSingleton(mongoDbIdentityConfiguration.MongoDbSettings); + services.AddSingleton(provider => + { + var options = provider.GetService(); + return mongoRepository ?? new MongoRepository(options.ConnectionString, options.DatabaseName); + }); + + CommonMongoDbSetup, TKey>(services, mongoDbIdentityConfiguration); + } + + public static void ConfigureMongoDbIdentity(this IServiceCollection services, MongoDbIdentityConfiguration mongoDbIdentityConfiguration) + where TUser : MongoIdentityUser, new() + { + services.AddSingleton(mongoDbIdentityConfiguration.MongoDbSettings); + services.AddSingleton(provider => + { + var options = provider.GetService(); + return new MongoRepository(options.ConnectionString, options.DatabaseName); + }); + + CommonMongoDbSetup(services, mongoDbIdentityConfiguration); + } + + + public static void ConfigureMongoDbIdentity(this IServiceCollection services, MongoDbIdentityConfiguration mongoDbIdentityConfiguration, + IMongoDbContext mongoDbContext = null) + where TUser : MongoIdentityUser, new() + where TRole : MongoIdentityRole, new() + where TKey : IEquatable + { + services.AddSingleton(mongoDbIdentityConfiguration.MongoDbSettings); + services.AddSingleton(provider => + { + var options = provider.GetService(); + return mongoDbContext == null ? new MongoRepository(options.ConnectionString, options.DatabaseName) : new MongoRepository(mongoDbContext); + }); + + CommonMongoDbSetup(services, mongoDbIdentityConfiguration); + } + + private static void CommonMongoDbSetup(this IServiceCollection services, MongoDbIdentityConfiguration mongoDbIdentityConfiguration) + where TUser : MongoIdentityUser, new() + where TRole : MongoIdentityRole, new() + where TKey : IEquatable + { + services.AddScoped>(provider => + { + var userStore = new MongoUserStore(provider.GetService().Context); + return userStore; + }); + + services.AddScoped>(provider => + { + return new MongoRoleStore(provider.GetService().Context); + }); + + services.AddIdentity() + .AddDefaultTokenProviders(); + + services.Configure(mongoDbIdentityConfiguration.IdentityOptionsAction); + } + + } +} diff --git a/src/GlobalVariables.cs b/src/GlobalVariables.cs new file mode 100644 index 0000000..83f619d --- /dev/null +++ b/src/GlobalVariables.cs @@ -0,0 +1,9 @@ +using System; + +namespace AspNetCore.Identity.MongoDbCore +{ + public static class GlobalVariables + { + public static Random Random = new Random(); + } +} diff --git a/src/Infrastructure/MongoRepository.cs b/src/Infrastructure/MongoRepository.cs new file mode 100644 index 0000000..a510851 --- /dev/null +++ b/src/Infrastructure/MongoRepository.cs @@ -0,0 +1,35 @@ +using MongoDbGenericRepository; + +namespace AspNetCore.Identity.MongoDbCore.Infrastructure +{ + public interface IMongoRepository : IBaseMongoRepository + { + void DropCollection(); + void DropCollection(string partitionKey); + IMongoDbContext Context { get; } + } + + + public class MongoRepository : BaseMongoRepository, IMongoRepository + { + public MongoRepository(string connectionString, string databaseName) : base(connectionString, databaseName) + { + } + + public MongoRepository(IMongoDbContext mongoDbContext) : base(mongoDbContext) + { + } + + public void DropCollection() + { + MongoDbContext.DropCollection(); + } + + public void DropCollection(string partitionKey) + { + MongoDbContext.DropCollection(partitionKey); + } + + public IMongoDbContext Context => MongoDbContext; + } +} diff --git a/src/Interfaces/IClaimHolder.cs b/src/Interfaces/IClaimHolder.cs new file mode 100644 index 0000000..dce1f8e --- /dev/null +++ b/src/Interfaces/IClaimHolder.cs @@ -0,0 +1,13 @@ +using AspNetCore.Identity.MongoDbCore.Models; +using System.Collections.Generic; + +namespace AspNetCore.Identity.MongoDbCore.Interfaces +{ + /// + /// The interface for an object that holds claims. + /// + public interface IClaimHolder + { + List Claims { get; set; } + } +} diff --git a/src/Models/MongoIdentityRole.cs b/src/Models/MongoIdentityRole.cs new file mode 100644 index 0000000..380c160 --- /dev/null +++ b/src/Models/MongoIdentityRole.cs @@ -0,0 +1,84 @@ +using AspNetCore.Identity.MongoDbCore.Interfaces; +using Microsoft.AspNetCore.Identity; +using MongoDbGenericRepository.Models; +using System; +using System.Collections.Generic; +using System.Security.Claims; + +namespace AspNetCore.Identity.MongoDbCore.Models +{ + public class MongoDbIdentityRole : MongoIdentityRole + { + public MongoDbIdentityRole() : base() + { + } + + public MongoDbIdentityRole(string roleName) : base(roleName) + { + } + } + + public class MongoIdentityRole : MongoIdentityRole + { + public MongoIdentityRole() : base() + { + } + + public MongoIdentityRole(string roleName) : base(roleName) + { + } + } + + public class MongoIdentityRole : IdentityRole, IDocument, IClaimHolder + where TKey : IEquatable + { + + private void InitializeFields() + { + Version = 1; + Claims = new List(); + Guid guidValue = Guid.NewGuid(); + var idTypeName = typeof(TKey).Name; + switch (idTypeName) + { + case "Guid": + Id = (TKey)(object)guidValue; + break; + case "Int32": + Id = (TKey)(object)GlobalVariables.Random.Next(1, int.MaxValue); + break; + case "String": + Id = (TKey)(object)guidValue.ToString(); + break; + } + } + + public MongoIdentityRole() + { + InitializeFields(); + } + + public MongoIdentityRole(string roleName) + { + Name = roleName; + InitializeFields(); + } + + public MongoIdentityRole(string name, TKey key) + { + InitializeFields(); + Id = key; + Name = Name; + } + + /// + /// The version of the role schema + /// + public int Version { get; set; } + + /// + /// The claims associated to the role + /// + public List Claims { get; set; } + } +} diff --git a/src/Models/MongoIdentityUser.cs b/src/Models/MongoIdentityUser.cs new file mode 100644 index 0000000..2490875 --- /dev/null +++ b/src/Models/MongoIdentityUser.cs @@ -0,0 +1,398 @@ +using System; +using System.Collections.Generic; +using MongoDbGenericRepository.Models; +using System.Linq; +using MongoDB.Driver; +using AspNetCore.Identity.MongoDbCore.Interfaces; +using Microsoft.AspNetCore.Identity; + +namespace AspNetCore.Identity.MongoDbCore.Models +{ + public class Token + { + /// + /// Gets or sets the LoginProvider this token is from. + /// + public string LoginProvider { get; set; } + /// + /// Gets or sets the name of the token. + public string Name { get; set; } + /// + /// Gets or sets the token value. + /// + public string Value { get; set; } + } + + public class MongoClaim { + public string Type { get; set; } + public string Value { get; set; } + public string Issuer { get; set; } + } + + public class UserRole + { + public object UserId { get; set; } + public object RoleId { get; set; } + } + + public class MongoDbIdentityUser : MongoIdentityUser + { + public MongoDbIdentityUser() : base() + { + } + + public MongoDbIdentityUser(string userName) : base(userName) + { + } + + public MongoDbIdentityUser(string userName, string email) : base(userName, email) + { + } + } + + public class MongoIdentityUser : MongoIdentityUser + { + public MongoIdentityUser() : base() + { + } + + public MongoIdentityUser(string userName) : base(userName) + { + } + + public MongoIdentityUser(string userName, string email) : base(userName, email) + { + } + } + + public class MongoIdentityUser : IdentityUser, IDocument, IClaimHolder + where TKey : IEquatable + { + + public int Version { get; set; } + + public DateTime CreatedOn { get; private set; } + public DateTime? LockoutEndDate { get; private set; } + public DateTime? DeletedOn { get; private set; } + public List Claims { get; set; } + public List Roles { get; set; } + public List Logins { get; set; } + public List Tokens { get; set; } + + private void InitializeFields() + { + Claims = new List(); + Logins = new List(); + Roles = new List(); + Tokens = new List(); + Guid guidValue = Guid.NewGuid(); + + var idTypeName = typeof(TKey).Name; + switch (idTypeName) + { + case "Guid": + Id = (TKey)(object)guidValue; + break; + case "Int32": + Id = (TKey)(object)GlobalVariables.Random.Next(1, int.MaxValue); + break; + case "String": + Id = (TKey)(object)guidValue.ToString(); + break; + } + + } + + public MongoIdentityUser() + { + CreatedOn = DateTime.UtcNow; + SetVersion(1); + InitializeFields(); + } + + public MongoIdentityUser(string userName, string email) : this(userName) + { + if (email != null) + { + Email = email.ToLowerInvariant().Trim(); + } + } + + public MongoIdentityUser(string userName) + { + UserName = userName ?? throw new ArgumentNullException(nameof(userName)); + CreatedOn = DateTime.UtcNow; + + SetVersion(1); + InitializeFields(); + Roles = new List(); + } + + public virtual MongoIdentityUser SetId(TKey key) + { + Id = key; + return this; + } + + public virtual MongoIdentityUser SetVersion(int version) + { + Version = 1; + return this; + } + + public virtual void EnableTwoFactorAuthentication() + { + TwoFactorEnabled = true; + } + + public virtual void DisableTwoFactorAuthentication() + { + TwoFactorEnabled = false; + } + + public virtual void EnableLockout() + { + LockoutEnabled = true; + } + + public virtual void DisableLockout() + { + LockoutEnabled = false; + } + + public virtual void SetEmail(string email) + { + Email = email ?? throw new ArgumentNullException(nameof(email)); + } + + public virtual void SetNormalizedUserName(string normalizedUserName) + { + NormalizedUserName = normalizedUserName ?? throw new ArgumentNullException(nameof(normalizedUserName)); + } + + public virtual void SetPhoneNumber(string phoneNumber) + { + PhoneNumber = phoneNumber; + } + + public virtual void SetPasswordHash(string passwordHash) + { + PasswordHash = passwordHash; + } + + public virtual void SetSecurityStamp(string securityStamp) + { + SecurityStamp = securityStamp; + } + + public virtual void SetAccessFailedCount(int accessFailedCount) + { + AccessFailedCount = accessFailedCount; + } + + public virtual void ResetAccessFailedCount() + { + AccessFailedCount = 0; + } + + public virtual void LockUntil(DateTime lockoutEndDate) + { + LockoutEndDate = lockoutEndDate; + } + + public void Delete() + { + if (DeletedOn != null) + { + throw new InvalidOperationException($"User '{Id}' has already been deleted."); + } + + DeletedOn = DateTime.UtcNow; + } + + #region Role Management + + public virtual IdentityUserRole GetUserRole(TKey roleId) + { + var foundRoleId = Roles.FirstOrDefault(e => e.Equals(roleId)); + if (!foundRoleId.Equals(default(TKey))) + { + return new IdentityUserRole + { + UserId = Id, + RoleId = foundRoleId + }; + } + return default(IdentityUserRole); + } + + public virtual bool RemoveRole(TKey roleId) + { + var roleClaim = Roles.FirstOrDefault(e => e.Equals(roleId)); + if (!roleClaim.Equals(default(TKey))) + { + Roles.Remove(roleId); + return true; + } + return false; + } + + public virtual bool AddRole(TKey roleId) + { + if (!Roles.Contains(roleId)) + { + Roles.Add(roleId); + return true; + } + return false; + } + + #endregion + + #region Login Management + + public virtual bool AddLogin(UserLoginInfo mongoUserLogin) + { + if (mongoUserLogin == null) + { + throw new ArgumentNullException(nameof(mongoUserLogin)); + } + if (HasLogin(mongoUserLogin)) + { + return false; + } + Logins.Add(mongoUserLogin); + return true; + } + + public virtual bool HasLogin(UserLoginInfo login) + { + return Logins.Any(e => e.LoginProvider == login.LoginProvider && e.ProviderKey == e.ProviderKey); + } + + public virtual void RemoveLogin(UserLoginInfo mongoUserLogin) + { + if (mongoUserLogin == null) + { + throw new ArgumentNullException(nameof(mongoUserLogin)); + } + + Logins.Remove(mongoUserLogin); + } + + public virtual IdentityUserLogin GetUserLogin(string loginProvider, string providerKey) + { + + var login = Logins.FirstOrDefault(e => e.LoginProvider == loginProvider && e.ProviderKey == providerKey); + if (login != null) + { + return new IdentityUserLogin + { + UserId = Id, + LoginProvider = login.LoginProvider, + ProviderDisplayName = login.ProviderDisplayName, + ProviderKey = login.ProviderKey + }; + } + return default(IdentityUserLogin); + } + + #endregion + + #region Token Management + + + /// + /// Replaces a claim on a claim holder, implementing . + /// + /// The object holding claims. + /// The claim you want to replace. + /// The new claim you want to set. + /// Returns true if the claim was replaced. + public bool SetToken(IdentityUserToken tokenToset, string value) + { + var token = Tokens.FirstOrDefault(e => e.LoginProvider == tokenToset.LoginProvider && e.Name == tokenToset.Name); + if (token != null) + { + token.Value = value; + return true; + } + return false; + } + + public IdentityUserToken GetToken(string loginProvider, string name) + { + var token = Tokens.FirstOrDefault(e => e.LoginProvider == loginProvider && e.Name == name); + if (token != null) + { + return new IdentityUserToken + { + UserId = Id, + LoginProvider = token.LoginProvider, + Name = token.Name, + Value = token.Value + }; + } + return default(IdentityUserToken); + } + + public bool HasToken(IdentityUserToken token) + { + return Tokens.Any(e => e.LoginProvider == token.LoginProvider + && e.Name == token.Name + && e.Value == token.Value); + } + + public bool AddOrSet(IdentityUserToken token) + { + var exists = GetToken(token.LoginProvider, token.Name); + if (exists != null && exists.Value != token.Value) + { + return SetToken(exists, token.Value); + } + if (exists == null) + { + Tokens.Add(new Token + { + LoginProvider = token.LoginProvider, + Name = token.Name, + Value = token.Value + }); + return true; + } + return false; + } + + public bool AddUserToken(TUserToken token) where TUserToken : IdentityUserToken + { + if (HasToken(token)) + { + return false; + } + + Tokens.Add(new Token + { + LoginProvider = token.LoginProvider, + Name = token.Name, + Value = token.Value + }); + return true; + } + + public bool RemoveUserToken(TUserToken token) where TUserToken : IdentityUserToken + { + var exists = Tokens.FirstOrDefault(e => e.LoginProvider == token.LoginProvider && e.Name == token.Name); + if (exists == null) + { + return false; + } + Tokens.Remove(exists); + return true; + } + + #endregion Token Management + + + + } +} \ No newline at end of file diff --git a/src/MongoConfig.cs b/src/MongoConfig.cs new file mode 100644 index 0000000..ad26b55 --- /dev/null +++ b/src/MongoConfig.cs @@ -0,0 +1,49 @@ +using AspNetCore.Identity.MongoDbCore.Models; +using MongoDB.Bson.Serialization.Conventions; +using System; +using System.Threading; + +namespace AspNetCore.Identity.MongoDbCore +{ + internal static class MongoConfig + { + private static bool _initialized = false; + private static object _initializationLock = new object(); + private static object _initializationTarget; + + public static void EnsureConfigured() + { + EnsureConfiguredImpl(); + } + + private static void EnsureConfiguredImpl() + { + LazyInitializer.EnsureInitialized(ref _initializationTarget, ref _initialized, ref _initializationLock, () => + { + Configure(); + return null; + }); + } + + private static void Configure() + { + RegisterConventions(); + } + + private static void RegisterConventions() + { + var pack = new ConventionPack + { + new IgnoreIfNullConvention(false), + new CamelCaseElementNameConvention(), + }; + + ConventionRegistry.Register("AspNetCore.Identity.MongoDB", pack, IsConventionApplicable); + } + + private static bool IsConventionApplicable(Type type) + { + return type == typeof(MongoIdentityUser<>); + } + } +} \ No newline at end of file diff --git a/src/MongoIdentityDbContext.cs b/src/MongoIdentityDbContext.cs new file mode 100644 index 0000000..21d5b0c --- /dev/null +++ b/src/MongoIdentityDbContext.cs @@ -0,0 +1,89 @@ +using AspNetCore.Identity.MongoDbCore.Infrastructure; +using AspNetCore.Identity.MongoDbCore.Models; +using MongoDB.Driver; +using MongoDbGenericRepository; +using System; + +namespace AspNetCore.Identity.MongoDbCore +{ + + public class MongoIdentityDbContext : MongoDbContext + { + + public MongoIdentityDbContext(string connectionString, string databaseName) : base(connectionString, databaseName) + { + } + + public IMongoRepository MongoRepository { get; } + + public IMongoCollection Users + { + get + { + return GetCollection(); + } + } + + public IMongoCollection Roles + { + get + { + return GetCollection(); + } + } + } + + public class MongoIdentityDbContext : MongoDbContext + where TUser : MongoIdentityUser + { + + public MongoIdentityDbContext(string connectionString, string databaseName) : base(connectionString, databaseName) + { + } + + public IMongoRepository MongoRepository { get; } + + public IMongoCollection Users + { + get + { + return GetCollection(); + } + } + + public IMongoCollection Roles + { + get + { + return GetCollection(); + } + } + } + + public class MongoIdentityDbContext : MongoDbContext + where TUser : MongoIdentityUser + where TRole : MongoIdentityRole + where TKey : IEquatable + { + + public MongoIdentityDbContext(string connectionString, string databaseName) : base(connectionString, databaseName) + { + } + + public IMongoCollection Users + { + get + { + return GetCollection(); + } + } + + public IMongoCollection Roles + { + get + { + return GetCollection(); + } + } + } +} diff --git a/src/MongoIdentityServiceCollectionExensions.cs b/src/MongoIdentityServiceCollectionExensions.cs new file mode 100644 index 0000000..1690382 --- /dev/null +++ b/src/MongoIdentityServiceCollectionExensions.cs @@ -0,0 +1,59 @@ +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.AspNetCore.Identity; + +namespace AspNetCore.Identity.MongoDbCore +{ + /// + /// Represents the password hashing options + /// + public sealed class PasswordHasherOptionsAccessor : IOptions + { + /// + /// Gets options which use the IdentityV3 compat mode, and set the iteration count to 200000 PBKDF2-SHA256 iterations + /// (roughly 200ms of work) + /// + public PasswordHasherOptions Value { get; } = new PasswordHasherOptions + { + CompatibilityMode = PasswordHasherCompatibilityMode.IdentityV3, + IterationCount = 200000 + }; + } + + public static class MongoIdentityServiceCollectionExtensions + { + public static IdentityBuilder AddIdentity(this IServiceCollection services) + where TUser : class => services.AddIdentity(null); + + public static IdentityBuilder AddIdentity(this IServiceCollection services, Action setupAction) + where TUser : class + { + // Hosting doesn't add IHttpContextAccessor by default + services.TryAddSingleton(); + + // Identity services + services.TryAddScoped, UserValidator>(); + services.TryAddScoped, PasswordValidator>(); + + services.TryAddScoped, PasswordHasher>(); + services.TryAddScoped(); + + // No interface for the error describer so we can add errors without rev'ing the interface + services.TryAddScoped(); + services.TryAddScoped>(); + services.TryAddScoped, UserClaimsPrincipalFactory>(); + services.TryAddScoped, AspNetUserManager>(); + services.TryAddScoped, SignInManager>(); + + if (setupAction != null) + { + services.Configure(setupAction); + } + + return new IdentityBuilder(typeof(TUser), services); + } + } +} diff --git a/src/MongoRoleStore.cs b/src/MongoRoleStore.cs new file mode 100644 index 0000000..81a5d6b --- /dev/null +++ b/src/MongoRoleStore.cs @@ -0,0 +1,463 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using MongoDbGenericRepository; +using MongoDB.Driver; +using AspNetCore.Identity.MongoDbCore.Extensions; +using AspNetCore.Identity.MongoDbCore.Models; +using AspNetCore.Identity.MongoDbCore.Infrastructure; +using Microsoft.AspNetCore.Identity; + +namespace AspNetCore.Identity.MongoDbCore +{ + /// + /// Creates a new instance of a persistence store for roles. + /// + /// The type of the class representing a role + public class MongoRoleStore : MongoRoleStore + where TRole : MongoIdentityRole + { + /// + /// Constructs a new instance of . + /// + /// The . + /// The . + public MongoRoleStore(IMongoDbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } + } + + /// + /// Creates a new instance of a persistence store for roles. + /// + /// The type of the class representing a role. + /// The type of the data context class used to access the store. + public class MongoRoleStore : MongoRoleStore + where TRole : MongoIdentityRole + where TContext : IMongoDbContext + { + /// + /// Constructs a new instance of . + /// + /// The . + /// The . + public MongoRoleStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } + } + + /// + /// Creates a new instance of a persistence store for roles. + /// + /// The type of the class representing a role. + /// The type of the data context class used to access the store. + /// The type of the primary key for a role. + public class MongoRoleStore : MongoRoleStore, IdentityRoleClaim>, + IQueryableRoleStore, + IRoleClaimStore + where TRole : MongoIdentityRole + where TKey : IEquatable + where TContext : IMongoDbContext + { + /// + /// Constructs a new instance of . + /// + /// The . + /// The . + public MongoRoleStore(IMongoDbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } + } + + /// + /// Creates a new instance of a persistence store for roles. + /// + /// The type of the class representing a role. + /// The type of the data context class used to access the store. + /// The type of the primary key for a role. + /// The type of the class representing a user role. + /// The type of the class representing a role claim. + public class MongoRoleStore : + IQueryableRoleStore, + IRoleClaimStore + where TRole : MongoIdentityRole + where TKey : IEquatable + where TContext : IMongoDbContext + where TUserRole : IdentityUserRole, new() + where TRoleClaim : IdentityRoleClaim, new() + { + /// + /// Constructs a new instance of . + /// + /// The . + /// The . + public MongoRoleStore(IMongoDbContext context, IdentityErrorDescriber describer = null) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + Context = context; + ErrorDescriber = describer ?? new IdentityErrorDescriber(); + } + + private bool _disposed; + + + /// + /// Gets the database context for this store. + /// + private static IMongoDbContext Context { get; set; } + + private static IMongoRepository _mongoRepository; + private static IMongoRepository MongoRepository + { + get + { + if (_mongoRepository == null) + { + _mongoRepository = new MongoRepository(Context); + } + return _mongoRepository; + } + } + + /// + /// Gets or sets the for any error that occurred with the current operation. + /// + public IdentityErrorDescriber ErrorDescriber { get; set; } + + /// + /// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called. + /// + /// + /// True if changes should be automatically persisted, otherwise false. + /// + public bool AutoSaveChanges { get; set; } = true; + + /// + /// Creates a new role in a store as an asynchronous operation. + /// + /// The role to create in the store. + /// The used to propagate notifications that the operation should be canceled. + /// A that represents the of the asynchronous query. + public async virtual Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + await MongoRepository.AddOneAsync(role); + return IdentityResult.Success; + } + + /// + /// Updates a role in a store as an asynchronous operation. + /// + /// The role to update in the store. + /// The used to propagate notifications that the operation should be canceled. + /// A that represents the of the asynchronous query. + public async virtual Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + var oldStamp = role.ConcurrencyStamp; + role.ConcurrencyStamp = Guid.NewGuid().ToString(); + var collection = MongoRepository.Context.GetCollection(); + var updateRes = await collection.ReplaceOneAsync(x => x.Id.Equals(role.Id) + && x.ConcurrencyStamp.Equals(oldStamp), + role); + if (updateRes.ModifiedCount == 0) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; + } + + /// + /// Deletes a role from the store as an asynchronous operation. + /// + /// The role to delete from the store. + /// The used to propagate notifications that the operation should be canceled. + /// A that represents the of the asynchronous query. + public async virtual Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + var oldStamp = role.ConcurrencyStamp; + role.ConcurrencyStamp = Guid.NewGuid().ToString(); + var collection = MongoRepository.Context.GetCollection(); + var deleteRes = await collection.DeleteOneAsync(x => x.Id.Equals(role.Id) + && x.ConcurrencyStamp.Equals(oldStamp)); + if (deleteRes.DeletedCount == 0) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; + } + + /// + /// Gets the ID for a role from the store as an asynchronous operation. + /// + /// The role whose ID should be returned. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the ID of the role. + public virtual Task GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + return Task.FromResult(ConvertIdToString(role.Id)); + } + + /// + /// Gets the name of a role from the store as an asynchronous operation. + /// + /// The role whose name should be returned. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the name of the role. + public virtual Task GetRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + return Task.FromResult(role.Name); + } + + /// + /// Sets the name of a role in the store as an asynchronous operation. + /// + /// The role whose name should be set. + /// The name of the role. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + if (role.Name != roleName) + { + role.Name = roleName; + return MongoRepository.UpdateOneAsync(role, x => x.Name, role.Name); + } + + return Task.CompletedTask; + } + + /// + /// Converts the provided to a strongly typed key object. + /// + /// The id to convert. + /// An instance of representing the provided . + public virtual TKey ConvertIdFromString(string id) + { + if (id == null) + { + return default(TKey); + } + return (TKey)TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(id); + } + + /// + /// Converts the provided to its string representation. + /// + /// The id to convert. + /// An representation of the provided . + public virtual string ConvertIdToString(TKey id) + { + if (id == null) + { + return null; + } + + if (id.Equals(default(TKey))) + { + return null; + } + return id.ToString(); + } + + /// + /// Finds the role who has the specified ID as an asynchronous operation. + /// + /// The role ID to look for. + /// The used to propagate notifications that the operation should be canceled. + /// A that result of the look up. + public virtual Task FindByIdAsync(string id, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var roleId = ConvertIdFromString(id); + return MongoRepository.GetOneAsync(u => u.Id.Equals(roleId)); + } + + /// + /// Finds the role who has the specified normalized name as an asynchronous operation. + /// + /// The normalized role name to look for. + /// The used to propagate notifications that the operation should be canceled. + /// A that result of the look up. + public virtual Task FindByNameAsync(string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + return MongoRepository.GetOneAsync(r => r.NormalizedName == normalizedName); + } + + /// + /// Get a role's normalized name as an asynchronous operation. + /// + /// The role whose normalized name should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the name of the role. + public virtual Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + return Task.FromResult(role.NormalizedName); + } + + /// + /// Set a role's normalized name as an asynchronous operation. + /// + /// The role whose normalized name should be set. + /// The normalized name to set + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + if (role.NormalizedName != normalizedName) + { + role.NormalizedName = normalizedName; + return MongoRepository.UpdateOneAsync(role, x => x.NormalizedName, role.NormalizedName); + } + return Task.CompletedTask; + } + + /// + /// Throws if this class has been disposed. + /// + protected void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + /// + /// Dispose the stores + /// + public void Dispose() => _disposed = true; + + /// + /// Get the claims associated with the specified as an asynchronous operation. + /// + /// The role whose claims should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the claims granted to a role. + public async virtual Task> GetClaimsAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + return role.Claims.Select(e => e.ToClaim()).ToList(); + } + + /// + /// Adds the given to the specified . + /// + /// The role to add the claim to. + /// The claim to add to the role. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task AddClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + if (role.AddClaim(claim)) + { + MongoRepository.UpdateOne>(role, e => e.Claims, role.Claims); + } + return Task.FromResult(false); + } + + /// + /// Removes the given from the specified . + /// + /// The role to remove the claim from. + /// The claim to remove from the role. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public async virtual Task RemoveClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (role == null) + { + throw new ArgumentNullException(nameof(role)); + } + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + if (role.RemoveClaim(claim)) + { + await MongoRepository.UpdateOneAsync>(role, e => e.Claims, role.Claims); + } + } + + /// + /// A navigation property for the roles the store contains. + /// + public virtual IQueryable Roles => Context.GetCollection().AsQueryable(); + + /// + /// Creates a entity representing a role claim. + /// + /// The associated role. + /// The associated claim. + /// The role claim entity. + protected virtual TRoleClaim CreateRoleClaim(TRole role, Claim claim) + => new TRoleClaim { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value }; + } +} \ No newline at end of file diff --git a/src/MongoUserOnlyStore.cs b/src/MongoUserOnlyStore.cs new file mode 100644 index 0000000..f235cc2 --- /dev/null +++ b/src/MongoUserOnlyStore.cs @@ -0,0 +1,1117 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using MongoDbGenericRepository; +using MongoDB.Driver; +using AspNetCore.Identity.MongoDbCore.Extensions; +using AspNetCore.Identity.MongoDbCore.Models; +using AspNetCore.Identity.MongoDbCore.Infrastructure; +using Microsoft.AspNetCore.Identity; + +namespace AspNetCore.Identity.MongoDbCore +{ + /// + /// Creates a new instance of a persistence store for the specified user type. + /// + /// The type representing a user. + public class MongoUserOnlyStore : MongoUserOnlyStore + where TUser : MongoIdentityUser, new() + { + /// + /// Constructs a new instance of . + /// + /// The . + /// The . + public MongoUserOnlyStore(IMongoDbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } + } + + /// + /// Represents a new instance of a persistence store for the specified user and role types. + /// + /// The type representing a user. + /// The type of the data context class used to access the store. + public class MongoUserOnlyStore : MongoUserOnlyStore + where TUser : MongoIdentityUser + where TContext : IMongoDbContext + { + /// + /// Constructs a new instance of . + /// + /// The . + /// The . + public MongoUserOnlyStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } + } + + /// + /// Represents a new instance of a persistence store for the specified user and role types. + /// + /// The type representing a user. + /// The type of the data context class used to access the store. + /// The type of the primary key for a role. + public class MongoUserOnlyStore : MongoUserOnlyStore, IdentityUserLogin, IdentityUserToken> + where TUser : MongoIdentityUser + where TContext : IMongoDbContext + where TKey : IEquatable + { + /// + /// Constructs a new instance of . + /// + /// The . + /// The . + public MongoUserOnlyStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } + } + + /// + /// Represents a new instance of a persistence store for the specified user and role types. + /// + /// The type representing a user. + /// The type of the data context class used to access the store. + /// The type of the primary key for a role. + /// The type representing a claim. + /// The type representing a user role. + /// The type representing a user external login. + /// The type representing a user token. + /// The type representing a role claim. + public class MongoUserOnlyStore : + UserStoreBase, + IUserAuthenticationTokenStore + where TUser : MongoIdentityUser + where TContext : IMongoDbContext + where TKey : IEquatable + where TUserClaim : IdentityUserClaim, new() + where TUserLogin : IdentityUserLogin, new() + where TUserToken : IdentityUserToken, new() + { + /// + /// Creates a new instance of the store. + /// + /// The context used to access the store. + /// The used to describe store errors. + public MongoUserOnlyStore(TContext context, IdentityErrorDescriber describer = null) : base(describer ?? new IdentityErrorDescriber()) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + Context = context; + } + + /// + /// Gets the database context for this store. + /// + private static TContext Context { get; set; } + + private static IMongoRepository _mongoRepository; + private static IMongoRepository MongoRepository + { + get + { + if (_mongoRepository == null) + { + _mongoRepository = new MongoRepository(Context); + } + return _mongoRepository; + } + } + + private IMongoCollection UsersSet { get { return Context.GetCollection(); } } + + /// + /// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called. + /// + /// + /// True if changes should be automatically persisted, otherwise false. + /// + public bool AutoSaveChanges { get; set; } = true; + + /// Saves the current store. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + protected Task SaveChanges(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + /// + /// Creates the specified in the user store. + /// + /// The user to create. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the creation operation. + public async override Task CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + await UsersSet.InsertOneAsync(user); + await SaveChanges(cancellationToken); + return IdentityResult.Success; + } + + /// + /// Updates the specified in the user store. + /// + /// The user to update. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the update operation. + public async override Task UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var oldStamp = user.ConcurrencyStamp; + user.ConcurrencyStamp = Guid.NewGuid().ToString(); + var collection = MongoRepository.Context.GetCollection(); + var updateRes = await collection.ReplaceOneAsync(x => x.Id.Equals(user.Id) + && x.ConcurrencyStamp.Equals(oldStamp), + user); + if(updateRes.ModifiedCount == 0) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; + } + + /// + /// Deletes the specified from the user store. + /// + /// The user to delete. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the update operation. + public async override Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.Claims.Clear(); + user.Roles.Clear(); + user.Logins.Clear(); + user.Tokens.Clear(); + var oldStamp = user.ConcurrencyStamp; + user.ConcurrencyStamp = Guid.NewGuid().ToString(); + var collection = MongoRepository.Context.GetCollection(); + var deleteRes = await collection.DeleteOneAsync(x => x.Id.Equals(user.Id) + && x.ConcurrencyStamp.Equals(oldStamp)); + if (deleteRes.DeletedCount == 0) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; + } + + /// + /// Finds and returns a user, if any, who has the specified . + /// + /// The user ID to search for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var id = ConvertIdFromString(userId); + return MongoRepository.GetByIdAsync(id); + } + + /// + /// Finds and returns a user, if any, who has the specified normalized user name. + /// + /// The normalized user name to search for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + public override Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + return MongoRepository.GetOneAsync(u => u.NormalizedUserName == normalizedUserName); + } + + /// + /// A navigation property for the users the store contains. + /// + public override IQueryable Users + { + get { return UsersSet.AsQueryable(); } + } + + /// + /// Return a user with the matching userId if it exists. + /// + /// The user's id. + /// The used to propagate notifications that the operation should be canceled. + /// The user if it exists. + protected override Task FindUserAsync(TKey userId, CancellationToken cancellationToken) + { + return MongoRepository.GetOneAsync(u => u.Id.Equals(userId)); + } + + /// + /// Return a user login with the matching userId, provider, providerKey if it exists. + /// + /// The user's id. + /// The login provider name. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The user login if it exists. + protected override Task FindUserLoginAsync(TKey userId, string loginProvider, string providerKey, CancellationToken cancellationToken) + { + var user = MongoRepository.GetOne(x => x.Id.Equals(userId) && x.Logins.Any(e => e.LoginProvider == loginProvider && e.ProviderKey == providerKey)); + if (user != null) + { + return Task.FromResult((TUserLogin)user.GetUserLogin(loginProvider, providerKey)); + } + return Task.FromResult(default(TUserLogin)); + } + + /// + /// Return a user login with provider, providerKey if it exists. + /// + /// The login provider name. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The user login if it exists. + protected override Task FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) + { + var user = MongoRepository.GetOne(x => x.Logins.Any(e => e.LoginProvider == loginProvider && e.ProviderKey == providerKey)); + if (user != null) + { + return Task.FromResult((TUserLogin)user.GetUserLogin(loginProvider, providerKey)); + } + return Task.FromResult(default(TUserLogin)); + } + + /// + /// Get the claims associated with the specified as an asynchronous operation. + /// + /// The user whose claims should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the claims granted to a user. + public async override Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return user.Claims.Select(e => e.ToClaim()).ToList(); + } + + /// + /// Adds the given to the specified . + /// + /// The user to add the claim to. + /// The claim to add to the user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (claims == null) + { + throw new ArgumentNullException(nameof(claims)); + } + var addedSome = false; + foreach (var claim in claims) + { + if (user.AddClaim(claim)) + { + addedSome |= true; + } + } + if (addedSome) + { + var success = MongoRepository.UpdateOne>(user, p => p.Claims, user.Claims); + if (!success) + { + throw new Exception($"Failed to add claims to user {user.Id.ToString()}"); + } + } + return Task.FromResult(false); + } + + /// + /// Replaces the on the specified , with the . + /// + /// The user to replace the claim on. + /// The claim replace. + /// The new claim replacing the . + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public async override Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + if (newClaim == null) + { + throw new ArgumentNullException(nameof(newClaim)); + } + + if (user.ReplaceClaim(claim, newClaim)) + { + await MongoRepository.UpdateOneAsync>(user, e => e.Claims, user.Claims); + } + } + + /// + /// Removes the given from the specified . + /// + /// The user to remove the claims from. + /// The claim to remove. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public async override Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (claims == null) + { + throw new ArgumentNullException(nameof(claims)); + } + if (user.RemoveClaims(claims)) + { + await MongoRepository.UpdateOneAsync>(user, e => e.Claims, user.Claims); + } + } + + /// + /// Adds the given to the specified . + /// + /// The user to add the login to. + /// The login to add to the user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task AddLoginAsync(TUser user, UserLoginInfo login, + CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (login == null) + { + throw new ArgumentNullException(nameof(login)); + } + + if (user.AddLogin(login)) + { + MongoRepository.UpdateOne>(user, e => e.Logins, user.Logins); + } + + return Task.FromResult(false); + } + + /// + /// Removes the given from the specified . + /// + /// The user to remove the login from. + /// The login to remove from the user. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override async Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, + CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var entry = user.Logins.FirstOrDefault(e => e.LoginProvider == loginProvider && e.ProviderKey == providerKey); + if (entry != null) + { + user.RemoveLogin(entry); + } + } + + /// + /// Retrieves the associated logins for the specified . + /// + /// The user whose associated logins to retrieve. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The for the asynchronous operation, containing a list of for the specified , if any. + /// + public async override Task> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return user.Logins.ToList(); + } + + /// + /// Retrieves the user associated with the specified login provider and login provider key. + /// + /// The login provider who provided the . + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The for the asynchronous operation, containing the user, if any which matched the specified login provider and key. + /// + public async override Task FindByLoginAsync(string loginProvider, string providerKey, + CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var userLogin = await FindUserLoginAsync(loginProvider, providerKey, cancellationToken); + if (userLogin != null) + { + return await FindUserAsync(userLogin.UserId, cancellationToken); + } + return null; + } + + /// + /// Gets the user, if any, associated with the specified, normalized email address. + /// + /// The normalized email address to return the user for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address. + /// + public override Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + return MongoRepository.GetOneAsync(u => u.NormalizedEmail == normalizedEmail); + } + + /// + /// Retrieves all users with the specified claim. + /// + /// The claim whose users should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The contains a list of users, if any, that contain the specified claim. + /// + public async override Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + var filter = Builders.Filter.ElemMatch(x => x.Claims, userClaims => userClaims.Value.Equals(claim.Value) + && userClaims.Type.Equals(claim.Type)); + var collection = MongoRepository.Context.GetCollection(); + var cursor = collection.Find(filter); + var res = await cursor.ToListAsync(); + return res; + } + + #region Token Management + + public class TokenProjection + { + public List Tokens { get; set; } + } + /// + /// Find a user token if it exists. + /// + /// The token owner. + /// The login provider for the token. + /// The name of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The user token if it exists. + protected override Task FindTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + return Task.FromResult((TUserToken)user.GetToken(loginProvider, name)); + } + + /// + /// Add a new user token. + /// + /// The token to be added. + /// + protected override Task AddUserTokenAsync(TUserToken token) + { + var user = MongoRepository.GetById(token.UserId); + if (user != null) + { + if (user.AddUserToken(token)) + { + MongoRepository.UpdateOne>(user, e => e.Tokens, user.Tokens); + } + } + return Task.CompletedTask; + } + + /// + /// Remove a new user token. + /// + /// The token to be removed. + /// + protected override Task RemoveUserTokenAsync(TUserToken token) + { + var user = MongoRepository.GetById(token.UserId); + if (user != null) + { + if (user.RemoveUserToken(token)) + { + MongoRepository.UpdateOne>(user, e => e.Tokens, user.Tokens); + } + } + return Task.CompletedTask; + } + + #endregion Token Management + + #region UserStoreBase overrides + + /// + /// Sets the given for the specified . + /// + /// The user whose name should be set. + /// The user name to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.UserName != userName) + { + user.UserName = userName; + MongoRepository.UpdateOne(user, e => e.UserName, user.UserName); + } + return Task.CompletedTask; + } + + /// + /// Sets the given normalized name for the specified . + /// + /// The user whose name should be set. + /// The normalized name to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.NormalizedUserName != normalizedName) + { + user.NormalizedUserName = normalizedName; + MongoRepository.UpdateOne(user, e => e.NormalizedUserName, user.NormalizedUserName); + } + return Task.CompletedTask; + } + + /// + /// Sets the password hash for a user. + /// + /// The user to set the password hash for. + /// The password hash to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.PasswordHash != passwordHash) + { + user.PasswordHash = passwordHash; + MongoRepository.UpdateOne(user, e => e.PasswordHash, user.PasswordHash); + } + return Task.CompletedTask; + } + + /// + /// Sets the flag indicating whether the specified 's email address has been confirmed or not. + /// + /// The user whose email confirmation status should be set. + /// A flag indicating if the email address has been confirmed, true if the address is confirmed otherwise false. + /// The used to propagate notifications that the operation should be canceled. + /// The task object representing the asynchronous operation. + public override Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (user.EmailConfirmed != confirmed) + { + user.EmailConfirmed = confirmed; + MongoRepository.UpdateOne(user, e => e.EmailConfirmed, user.EmailConfirmed); + } + return Task.CompletedTask; + } + + /// + /// Sets the address for a . + /// + /// The user whose email should be set. + /// The email to set. + /// The used to propagate notifications that the operation should be canceled. + /// The task object representing the asynchronous operation. + public override Task SetEmailAsync(TUser user, string email, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.Email != email) + { + user.Email = email; + MongoRepository.UpdateOne(user, e => e.Email, user.Email); + } + return Task.CompletedTask; + } + + /// + /// Sets the normalized email for the specified . + /// + /// The user whose email address to set. + /// The normalized email to set for the specified . + /// The used to propagate notifications that the operation should be canceled. + /// The task object representing the asynchronous operation. + public override Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.NormalizedEmail != normalizedEmail) + { + user.NormalizedEmail = normalizedEmail; + MongoRepository.UpdateOne(user, e => e.NormalizedEmail, user.NormalizedEmail); + } + user.NormalizedEmail = normalizedEmail; + return Task.CompletedTask; + } + + /// + /// Locks out a user until the specified end date has passed. Setting a end date in the past immediately unlocks a user. + /// + /// The user whose lockout date should be set. + /// The after which the 's lockout should end. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.LockoutEnd != lockoutEnd) + { + user.LockoutEnd = lockoutEnd; + MongoRepository.UpdateOne(user, e => e.LockoutEnd, user.LockoutEnd); + } + return Task.CompletedTask; + } + + /// + /// Records that a failed access has occurred, incrementing the failed access count. + /// + /// The user whose cancellation count should be incremented. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the incremented failed access count. + public override Task IncrementAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.AccessFailedCount++; + MongoRepository.UpdateOne(user, e => e.AccessFailedCount, user.AccessFailedCount); + return Task.FromResult(user.AccessFailedCount); + } + + /// + /// Resets a user's failed access count. + /// + /// The user whose failed access count should be reset. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + /// This is typically called after the account is successfully accessed. + public override Task ResetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.AccessFailedCount != 0) + { + user.AccessFailedCount = 0; + MongoRepository.UpdateOne(user, e => e.AccessFailedCount, user.AccessFailedCount); + } + return Task.CompletedTask; + } + + /// + /// Set the flag indicating if the specified can be locked out.. + /// + /// The user whose ability to be locked out should be set. + /// A flag indicating if lock out can be enabled for the specified . + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetLockoutEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.LockoutEnabled != enabled) + { + user.LockoutEnabled = enabled; + MongoRepository.UpdateOne(user, e => e.LockoutEnabled, user.LockoutEnabled); + } + return Task.CompletedTask; + } + + /// + /// Sets the telephone number for the specified . + /// + /// The user whose telephone number should be set. + /// The telephone number to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetPhoneNumberAsync(TUser user, string phoneNumber, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (user.PhoneNumber != phoneNumber) + { + user.PhoneNumber = phoneNumber; + MongoRepository.UpdateOne(user, e => e.PhoneNumber, user.PhoneNumber); + } + return Task.CompletedTask; + } + + /// + /// Sets a flag indicating if the specified 's phone number has been confirmed.. + /// + /// The user whose telephone number confirmation status should be set. + /// A flag indicating whether the user's telephone number has been confirmed. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetPhoneNumberConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.PhoneNumberConfirmed != confirmed) + { + user.PhoneNumberConfirmed = confirmed; + MongoRepository.UpdateOne(user, e => e.PhoneNumberConfirmed, user.PhoneNumberConfirmed); + } + return Task.CompletedTask; + } + + + /// + /// Sets the provided security for the specified . + /// + /// The user whose security stamp should be set. + /// The security stamp to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetSecurityStampAsync(TUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (stamp == null) + { + throw new ArgumentNullException(nameof(stamp)); + } + + if (user.SecurityStamp != stamp) + { + user.SecurityStamp = stamp; + MongoRepository.UpdateOne(user, e => e.SecurityStamp, user.SecurityStamp); + } + return Task.CompletedTask; + } + + /// + /// Sets a flag indicating whether the specified has two factor authentication enabled or not, + /// as an asynchronous operation. + /// + /// The user whose two factor authentication enabled status should be set. + /// A flag indicating whether the specified has two factor authentication enabled. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetTwoFactorEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (user.TwoFactorEnabled != enabled) + { + user.TwoFactorEnabled = enabled; + MongoRepository.UpdateOne(user, e => e.TwoFactorEnabled, user.TwoFactorEnabled); + } + return Task.CompletedTask; + } + + /// + /// Sets the token value for a particular user. + /// + /// The user. + /// The authentication provider for the token. + /// The name of the token. + /// The value of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override async Task SetTokenAsync(TUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var token = await FindTokenAsync(user, loginProvider, name, cancellationToken); + if (token == null) + { + if (user.AddUserToken(CreateUserToken(user, loginProvider, name, value))) + { + MongoRepository.UpdateOne>(user, e => e.Tokens, user.Tokens); + } + //await AddUserTokenAsync(CreateUserToken(user, loginProvider, name, value)); + } + else + { + if (user.SetToken(token, value)) + { + MongoRepository.UpdateOne>(user, e => e.Tokens, user.Tokens); + } + } + } + + /// + /// Deletes a token for a user. + /// + /// The user. + /// The authentication provider for the token. + /// The name of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override async Task RemoveTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var entry = await FindTokenAsync(user, loginProvider, name, cancellationToken); + if (entry != null) + { + if (user.RemoveUserToken(entry)) + { + MongoRepository.UpdateOne>(user, e => e.Tokens, user.Tokens); + } + } + } + + /// + /// Returns the token value. + /// + /// The user. + /// The authentication provider for the token. + /// The name of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override async Task GetTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var entry = await FindTokenAsync(user, loginProvider, name, cancellationToken); + return entry?.Value; + } + + private const string InternalLoginProvider = "[AspNetUserStore]"; + private const string AuthenticatorKeyTokenName = "AuthenticatorKey"; + private const string RecoveryCodeTokenName = "RecoveryCodes"; + + /// + /// Sets the authenticator key for the specified . + /// + /// The user whose authenticator key should be set. + /// The authenticator key to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetAuthenticatorKeyAsync(TUser user, string key, CancellationToken cancellationToken) + => SetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, key, cancellationToken); + + /// + /// Get the authenticator key for the specified . + /// + /// The user whose security stamp should be set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the security stamp for the specified . + public override Task GetAuthenticatorKeyAsync(TUser user, CancellationToken cancellationToken) + => GetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, cancellationToken); + + /// + /// Returns how many recovery code are still valid for a user. + /// + /// The user who owns the recovery code. + /// The used to propagate notifications that the operation should be canceled. + /// The number of valid recovery codes for the user.. + public override async Task CountCodesAsync(TUser user, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var mergedCodes = await GetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, cancellationToken) ?? ""; + if (mergedCodes.Length > 0) + { + return mergedCodes.Split(';').Length; + } + return 0; + } + + /// + /// Updates the recovery codes for the user while invalidating any previous recovery codes. + /// + /// The user to store new recovery codes for. + /// The new recovery codes for the user. + /// The used to propagate notifications that the operation should be canceled. + /// The new recovery codes for the user. + public override Task ReplaceCodesAsync(TUser user, IEnumerable recoveryCodes, CancellationToken cancellationToken) + { + var mergedCodes = string.Join(";", recoveryCodes); + return SetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, mergedCodes, cancellationToken); + } + + /// + /// Returns whether a recovery code is valid for a user. Note: recovery codes are only valid + /// once, and will be invalid after use. + /// + /// The user who owns the recovery code. + /// The recovery code to use. + /// The used to propagate notifications that the operation should be canceled. + /// True if the recovery code was found for the user. + public override async Task RedeemCodeAsync(TUser user, string code, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (code == null) + { + throw new ArgumentNullException(nameof(code)); + } + + var mergedCodes = await GetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, cancellationToken) ?? ""; + var splitCodes = mergedCodes.Split(';'); + if (splitCodes.Contains(code)) + { + var updatedCodes = new List(splitCodes.Where(s => s != code)); + await ReplaceCodesAsync(user, updatedCodes, cancellationToken); + return true; + } + return false; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/MongoUserStore.cs b/src/MongoUserStore.cs new file mode 100644 index 0000000..2717203 --- /dev/null +++ b/src/MongoUserStore.cs @@ -0,0 +1,1319 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using MongoDbGenericRepository; +using MongoDB.Driver; +using AspNetCore.Identity.MongoDbCore.Extensions; +using AspNetCore.Identity.MongoDbCore.Models; +using AspNetCore.Identity.MongoDbCore.Infrastructure; +using Microsoft.AspNetCore.Identity; + +namespace AspNetCore.Identity.MongoDbCore +{ + /// + /// Represents a new instance of a persistence store for users, using the default implementation + /// of with a string as a primary key. + /// + public class MongoUserStore : MongoUserStore> + { + /// + /// Constructs a new instance of . + /// + /// The . + /// The . + public MongoUserStore(IMongoDbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } + } + + /// + /// Creates a new instance of a persistence store for the specified user type. + /// + /// The type representing a user. + public class MongoUserStore : MongoUserStore, IMongoDbContext, string> + where TUser : MongoIdentityUser, new() + { + /// + /// Constructs a new instance of . + /// + /// The . + /// The . + public MongoUserStore(IMongoDbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } + } + + /// + /// Represents a new instance of a persistence store for the specified user and role types. + /// + /// The type representing a user. + /// The type representing a role. + /// The type of the data context class used to access the store. + public class MongoUserStore : MongoUserStore + where TUser : MongoIdentityUser, new() + where TRole : MongoIdentityRole, new() + where TContext : IMongoDbContext + { + /// + /// Constructs a new instance of . + /// + /// The . + /// The . + public MongoUserStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } + } + + /// + /// Represents a new instance of a persistence store for the specified user and role types. + /// + /// The type representing a user. + /// The type representing a role. + /// The type of the data context class used to access the store. + /// The type of the primary key for a role. + public class MongoUserStore : MongoUserStore, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim> + where TUser : MongoIdentityUser, new() + where TRole : MongoIdentityRole, new() + where TContext : IMongoDbContext + where TKey : IEquatable + { + /// + /// Constructs a new instance of . + /// + /// The . + /// The . + public MongoUserStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { } + } + + /// + /// Represents a new instance of a persistence store for the specified user and role types. + /// + /// The type representing a user. + /// The type representing a role. + /// The type of the data context class used to access the store. + /// The type of the primary key for a role. + /// The type representing a claim. + /// The type representing a user role. + /// The type representing a user external login. + /// The type representing a user token. + /// The type representing a role claim. + public class MongoUserStore : + UserStoreBase, + IUserAuthenticationTokenStore + where TUser : MongoIdentityUser, new() + where TRole : MongoIdentityRole, new() + where TContext : IMongoDbContext + where TKey : IEquatable + where TUserClaim : IdentityUserClaim, new() + where TUserRole : IdentityUserRole, new() + where TUserLogin : IdentityUserLogin, new() + where TUserToken : IdentityUserToken, new() + where TRoleClaim : IdentityRoleClaim, new() + { + /// + /// Creates a new instance of the store. + /// + /// The context used to access the store. + /// The used to describe store errors. + public MongoUserStore(TContext context, IdentityErrorDescriber describer = null) : base(describer ?? new IdentityErrorDescriber()) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + Context = context; + } + + /// + /// Gets the database context for this store. + /// + private static TContext Context { get; set; } + + private static IMongoRepository _mongoRepository; + private static IMongoRepository MongoRepository + { + get + { + if (_mongoRepository == null) + { + _mongoRepository = new MongoRepository(Context); + } + return _mongoRepository; + } + } + + private IMongoCollection UsersSet { get { return Context.GetCollection(); } } + private IMongoCollection Roles { get { return Context.GetCollection(); } } + + /// + /// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called. + /// + /// + /// True if changes should be automatically persisted, otherwise false. + /// + public bool AutoSaveChanges { get; set; } = true; + + /// Saves the current store. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + protected Task SaveChanges(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + /// + /// Creates the specified in the user store. + /// + /// The user to create. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the creation operation. + public async override Task CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + await UsersSet.InsertOneAsync(user); + await SaveChanges(cancellationToken); + return IdentityResult.Success; + } + + /// + /// Updates the specified in the user store. + /// + /// The user to update. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the update operation. + public async override Task UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var oldStamp = user.ConcurrencyStamp; + user.ConcurrencyStamp = Guid.NewGuid().ToString(); + var collection = MongoRepository.Context.GetCollection(); + var updateRes = await collection.ReplaceOneAsync(x => x.Id.Equals(user.Id) + && x.ConcurrencyStamp.Equals(oldStamp), + user); + if(updateRes.ModifiedCount == 0) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; + } + + /// + /// Deletes the specified from the user store. + /// + /// The user to delete. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the update operation. + public async override Task DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.Claims.Clear(); + user.Roles.Clear(); + user.Logins.Clear(); + user.Tokens.Clear(); + var oldStamp = user.ConcurrencyStamp; + user.ConcurrencyStamp = Guid.NewGuid().ToString(); + var collection = MongoRepository.Context.GetCollection(); + var deleteRes = await collection.DeleteOneAsync(x => x.Id.Equals(user.Id) + && x.ConcurrencyStamp.Equals(oldStamp)); + if (deleteRes.DeletedCount == 0) + { + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + return IdentityResult.Success; + } + + /// + /// Finds and returns a user, if any, who has the specified . + /// + /// The user ID to search for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var id = ConvertIdFromString(userId); + return MongoRepository.GetByIdAsync(id); + } + + /// + /// Finds and returns a user, if any, who has the specified normalized user name. + /// + /// The normalized user name to search for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + public override Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + return MongoRepository.GetOneAsync(u => u.NormalizedUserName == normalizedUserName); + } + + /// + /// A navigation property for the users the store contains. + /// + public override IQueryable Users + { + get { return UsersSet.AsQueryable(); } + } + + /// + /// Return a role with the normalized name if it exists. + /// + /// The normalized role name. + /// The used to propagate notifications that the operation should be canceled. + /// The role if it exists. + protected override Task FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken) + { + return MongoRepository.GetOneAsync(u => u.NormalizedName == normalizedRoleName); + } + + /// + /// Return a user role for the userId and roleId if it exists. + /// + /// The user's id. + /// The role's id. + /// The used to propagate notifications that the operation should be canceled. + /// The user role if it exists. + protected override Task FindUserRoleAsync(TKey userId, TKey roleId, CancellationToken cancellationToken) + { + var userRole = MongoRepository.ProjectOne( + x => x.Id.Equals(userId) && x.Roles.Any(r => r.Equals(roleId)), + x => new TUserRole + { + UserId = x.Id, + RoleId = roleId + }); + if (userRole != null) + { + return Task.FromResult(userRole); + } + return Task.FromResult(default(TUserRole)); + } + + /// + /// Return a user with the matching userId if it exists. + /// + /// The user's id. + /// The used to propagate notifications that the operation should be canceled. + /// The user if it exists. + protected override Task FindUserAsync(TKey userId, CancellationToken cancellationToken) + { + return MongoRepository.GetOneAsync(u => u.Id.Equals(userId)); + } + + /// + /// Return a user login with the matching userId, provider, providerKey if it exists. + /// + /// The user's id. + /// The login provider name. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The user login if it exists. + protected override Task FindUserLoginAsync(TKey userId, string loginProvider, string providerKey, CancellationToken cancellationToken) + { + var user = MongoRepository.GetOne(x => x.Id.Equals(userId) && x.Logins.Any(e => e.LoginProvider == loginProvider && e.ProviderKey == providerKey)); + if (user != null) + { + return Task.FromResult((TUserLogin)user.GetUserLogin(loginProvider, providerKey)); + } + return Task.FromResult(default(TUserLogin)); + } + + /// + /// Return a user login with provider, providerKey if it exists. + /// + /// The login provider name. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The user login if it exists. + protected override Task FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) + { + var user = MongoRepository.GetOne(x => x.Logins.Any(e => e.LoginProvider == loginProvider && e.ProviderKey == providerKey)); + if (user != null) + { + return Task.FromResult((TUserLogin)user.GetUserLogin(loginProvider, providerKey)); + } + return Task.FromResult(default(TUserLogin)); + } + + /// + /// Adds the given to the specified . + /// + /// The user to add the role to. + /// The role to add. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public async override Task AddToRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (string.IsNullOrWhiteSpace(normalizedRoleName)) + { + throw new ArgumentException("Value", nameof(normalizedRoleName)); + } + var roleEntity = await MongoRepository.GetOneAsync(x => x.NormalizedName == normalizedRoleName); + if (roleEntity == null) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.RoleNotFound, normalizedRoleName)); + } + + //if(user.Roles.Any(e => e.Equals(roleEntity.Id))) + //{ + // throw new InvalidOperationException($"User {user.Id} is already in role {roleEntity.Name}."); + //} + + if (user.AddRole(roleEntity.Id)) + { + MongoRepository.UpdateOne>(user, e => e.Roles, user.Roles); + } + } + + //private async Task UserAlreadyInRoleError(TUser user, string role) + //{ + // Logger.LogWarning(5, "User {userId} is already in role {role}.", await GetUserIdAsync(user), role); + // return IdentityResult.Failed(ErrorDescriber.UserAlreadyInRole(role)); + //} + + /// + /// Removes the given from the specified . + /// + /// The user to remove the role from. + /// The role to remove. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public async override Task RemoveFromRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (string.IsNullOrWhiteSpace(normalizedRoleName)) + { + throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, nameof(normalizedRoleName)); + } + var role = MongoRepository.GetOne(x => x.NormalizedName == normalizedRoleName); + + if (user.RemoveRole(role.Id)) + { + await MongoRepository.UpdateOneAsync(user); + } + } + + /// + /// Retrieves the roles the specified is a member of. + /// + /// The user whose roles should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the roles the user is a member of. + public override async Task> GetRolesAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (user.Roles.Any()) + { + return await MongoRepository.ProjectManyAsync(x => user.Roles.Contains(x.Id), x => x.Name); + } + return new List(); + } + + /// + /// Returns a flag indicating if the specified user is a member of the give . + /// + /// The user whose role membership should be checked. + /// The role to check membership of + /// The used to propagate notifications that the operation should be canceled. + /// A containing a flag indicating if the specified user is a member of the given group. If the + /// user is a member of the group the returned value with be true, otherwise it will be false. + public override async Task IsInRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (string.IsNullOrWhiteSpace(normalizedRoleName)) + { + throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, nameof(normalizedRoleName)); + } + var role = await MongoRepository.GetOneAsync(e => e.NormalizedName.Equals(normalizedRoleName)); + if (role != null) + { + return user.Roles.Any(r => r.Equals(role.Id)); + } + return false; + } + + /// + /// Get the claims associated with the specified as an asynchronous operation. + /// + /// The user whose claims should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the claims granted to a user. + public async override Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return user.Claims.Select(e => e.ToClaim()).ToList(); + } + + /// + /// Adds the given to the specified . + /// + /// The user to add the claim to. + /// The claim to add to the user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (claims == null) + { + throw new ArgumentNullException(nameof(claims)); + } + var addedSome = false; + foreach (var claim in claims) + { + if (user.AddClaim(claim)) + { + addedSome |= true; + } + } + if (addedSome) + { + var success = MongoRepository.UpdateOne>(user, p => p.Claims, user.Claims); + if (!success) + { + throw new Exception($"Failed to add claims to user {user.Id.ToString()}"); + } + } + return Task.FromResult(false); + } + + /// + /// Replaces the on the specified , with the . + /// + /// The user to replace the claim on. + /// The claim replace. + /// The new claim replacing the . + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public async override Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + if (newClaim == null) + { + throw new ArgumentNullException(nameof(newClaim)); + } + + if (user.ReplaceClaim(claim, newClaim)) + { + await MongoRepository.UpdateOneAsync>(user, e => e.Claims, user.Claims); + } + } + + /// + /// Removes the given from the specified . + /// + /// The user to remove the claims from. + /// The claim to remove. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public async override Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + { + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (claims == null) + { + throw new ArgumentNullException(nameof(claims)); + } + if (user.RemoveClaims(claims)) + { + await MongoRepository.UpdateOneAsync>(user, e => e.Claims, user.Claims); + } + } + + /// + /// Adds the given to the specified . + /// + /// The user to add the login to. + /// The login to add to the user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task AddLoginAsync(TUser user, UserLoginInfo login, + CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (login == null) + { + throw new ArgumentNullException(nameof(login)); + } + + if (user.AddLogin(login)) + { + MongoRepository.UpdateOne>(user, e => e.Logins, user.Logins); + } + + return Task.FromResult(false); + } + + /// + /// Removes the given from the specified . + /// + /// The user to remove the login from. + /// The login to remove from the user. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override async Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, + CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var entry = user.Logins.FirstOrDefault(e => e.LoginProvider == loginProvider && e.ProviderKey == providerKey); + if (entry != null) + { + user.RemoveLogin(entry); + } + } + + /// + /// Retrieves the associated logins for the specified . + /// + /// The user whose associated logins to retrieve. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The for the asynchronous operation, containing a list of for the specified , if any. + /// + public async override Task> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + return user.Logins.ToList(); + } + + /// + /// Retrieves the user associated with the specified login provider and login provider key. + /// + /// The login provider who provided the . + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The for the asynchronous operation, containing the user, if any which matched the specified login provider and key. + /// + public async override Task FindByLoginAsync(string loginProvider, string providerKey, + CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + var userLogin = await FindUserLoginAsync(loginProvider, providerKey, cancellationToken); + if (userLogin != null) + { + return await FindUserAsync(userLogin.UserId, cancellationToken); + } + return null; + } + + /// + /// Gets the user, if any, associated with the specified, normalized email address. + /// + /// The normalized email address to return the user for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address. + /// + public override Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + return MongoRepository.GetOneAsync(u => u.NormalizedEmail == normalizedEmail); + } + + /// + /// Retrieves all users with the specified claim. + /// + /// The claim whose users should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The contains a list of users, if any, that contain the specified claim. + /// + public async override Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + var filter = Builders.Filter.ElemMatch(x => x.Claims, userClaims => userClaims.Value.Equals(claim.Value) + && userClaims.Type.Equals(claim.Type)); + var collection = MongoRepository.Context.GetCollection(); + var cursor = collection.Find(filter); + var res = await cursor.ToListAsync(); + return res; + } + + /// + /// Retrieves all users in the specified role. + /// + /// The role whose users should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The contains a list of users, if any, that are in the specified role. + /// + public async override Task> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (string.IsNullOrEmpty(normalizedRoleName)) + { + throw new ArgumentNullException(nameof(normalizedRoleName)); + } + + var role = await FindRoleAsync(normalizedRoleName, cancellationToken); + + if (role != null) + { + return await MongoRepository.GetAllAsync(e => e.Roles.Contains(role.Id)); + } + return new List(); + } + + #region Token Management + + public class TokenProjection + { + public List Tokens { get; set; } + } + /// + /// Find a user token if it exists. + /// + /// The token owner. + /// The login provider for the token. + /// The name of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The user token if it exists. + protected override Task FindTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + return Task.FromResult((TUserToken)user.GetToken(loginProvider, name)); + } + + /// + /// Add a new user token. + /// + /// The token to be added. + /// + protected override Task AddUserTokenAsync(TUserToken token) + { + var user = MongoRepository.GetById(token.UserId); + if (user != null) + { + if (user.AddUserToken(token)) + { + MongoRepository.UpdateOne>(user, e => e.Tokens, user.Tokens); + } + } + return Task.CompletedTask; + } + + /// + /// Remove a new user token. + /// + /// The token to be removed. + /// + protected override Task RemoveUserTokenAsync(TUserToken token) + { + var user = MongoRepository.GetById(token.UserId); + if (user != null) + { + if (user.RemoveUserToken(token)) + { + MongoRepository.UpdateOne>(user, e => e.Tokens, user.Tokens); + } + } + return Task.CompletedTask; + } + + #endregion Token Management + + #region UserStoreBase overrides + + /// + /// Sets the given for the specified . + /// + /// The user whose name should be set. + /// The user name to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.UserName != userName) + { + user.UserName = userName; + MongoRepository.UpdateOne(user, e => e.UserName, user.UserName); + } + return Task.CompletedTask; + } + + /// + /// Sets the given normalized name for the specified . + /// + /// The user whose name should be set. + /// The normalized name to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.NormalizedUserName != normalizedName) + { + user.NormalizedUserName = normalizedName; + MongoRepository.UpdateOne(user, e => e.NormalizedUserName, user.NormalizedUserName); + } + return Task.CompletedTask; + } + + /// + /// Sets the password hash for a user. + /// + /// The user to set the password hash for. + /// The password hash to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.PasswordHash != passwordHash) + { + user.PasswordHash = passwordHash; + MongoRepository.UpdateOne(user, e => e.PasswordHash, user.PasswordHash); + } + return Task.CompletedTask; + } + + /// + /// Sets the flag indicating whether the specified 's email address has been confirmed or not. + /// + /// The user whose email confirmation status should be set. + /// A flag indicating if the email address has been confirmed, true if the address is confirmed otherwise false. + /// The used to propagate notifications that the operation should be canceled. + /// The task object representing the asynchronous operation. + public override Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (user.EmailConfirmed != confirmed) + { + user.EmailConfirmed = confirmed; + MongoRepository.UpdateOne(user, e => e.EmailConfirmed, user.EmailConfirmed); + } + return Task.CompletedTask; + } + + /// + /// Sets the address for a . + /// + /// The user whose email should be set. + /// The email to set. + /// The used to propagate notifications that the operation should be canceled. + /// The task object representing the asynchronous operation. + public override Task SetEmailAsync(TUser user, string email, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.Email != email) + { + user.Email = email; + MongoRepository.UpdateOne(user, e => e.Email, user.Email); + } + return Task.CompletedTask; + } + + /// + /// Sets the normalized email for the specified . + /// + /// The user whose email address to set. + /// The normalized email to set for the specified . + /// The used to propagate notifications that the operation should be canceled. + /// The task object representing the asynchronous operation. + public override Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.NormalizedEmail != normalizedEmail) + { + user.NormalizedEmail = normalizedEmail; + MongoRepository.UpdateOne(user, e => e.NormalizedEmail, user.NormalizedEmail); + } + user.NormalizedEmail = normalizedEmail; + return Task.CompletedTask; + } + + /// + /// Locks out a user until the specified end date has passed. Setting a end date in the past immediately unlocks a user. + /// + /// The user whose lockout date should be set. + /// The after which the 's lockout should end. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.LockoutEnd != lockoutEnd) + { + user.LockoutEnd = lockoutEnd; + MongoRepository.UpdateOne(user, e => e.LockoutEnd, user.LockoutEnd); + } + return Task.CompletedTask; + } + + /// + /// Records that a failed access has occurred, incrementing the failed access count. + /// + /// The user whose cancellation count should be incremented. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the incremented failed access count. + public override Task IncrementAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + user.AccessFailedCount++; + MongoRepository.UpdateOne(user, e => e.AccessFailedCount, user.AccessFailedCount); + return Task.FromResult(user.AccessFailedCount); + } + + /// + /// Resets a user's failed access count. + /// + /// The user whose failed access count should be reset. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + /// This is typically called after the account is successfully accessed. + public override Task ResetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.AccessFailedCount != 0) + { + user.AccessFailedCount = 0; + MongoRepository.UpdateOne(user, e => e.AccessFailedCount, user.AccessFailedCount); + } + return Task.CompletedTask; + } + + /// + /// Set the flag indicating if the specified can be locked out.. + /// + /// The user whose ability to be locked out should be set. + /// A flag indicating if lock out can be enabled for the specified . + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetLockoutEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.LockoutEnabled != enabled) + { + user.LockoutEnabled = enabled; + MongoRepository.UpdateOne(user, e => e.LockoutEnabled, user.LockoutEnabled); + } + return Task.CompletedTask; + } + + /// + /// Sets the telephone number for the specified . + /// + /// The user whose telephone number should be set. + /// The telephone number to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetPhoneNumberAsync(TUser user, string phoneNumber, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (user.PhoneNumber != phoneNumber) + { + user.PhoneNumber = phoneNumber; + MongoRepository.UpdateOne(user, e => e.PhoneNumber, user.PhoneNumber); + } + return Task.CompletedTask; + } + + /// + /// Sets a flag indicating if the specified 's phone number has been confirmed.. + /// + /// The user whose telephone number confirmation status should be set. + /// A flag indicating whether the user's telephone number has been confirmed. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetPhoneNumberConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if (user.PhoneNumberConfirmed != confirmed) + { + user.PhoneNumberConfirmed = confirmed; + MongoRepository.UpdateOne(user, e => e.PhoneNumberConfirmed, user.PhoneNumberConfirmed); + } + return Task.CompletedTask; + } + + + /// + /// Sets the provided security for the specified . + /// + /// The user whose security stamp should be set. + /// The security stamp to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetSecurityStampAsync(TUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (stamp == null) + { + throw new ArgumentNullException(nameof(stamp)); + } + + if (user.SecurityStamp != stamp) + { + user.SecurityStamp = stamp; + MongoRepository.UpdateOne(user, e => e.SecurityStamp, user.SecurityStamp); + } + return Task.CompletedTask; + } + + /// + /// Sets a flag indicating whether the specified has two factor authentication enabled or not, + /// as an asynchronous operation. + /// + /// The user whose two factor authentication enabled status should be set. + /// A flag indicating whether the specified has two factor authentication enabled. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetTwoFactorEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (user.TwoFactorEnabled != enabled) + { + user.TwoFactorEnabled = enabled; + MongoRepository.UpdateOne(user, e => e.TwoFactorEnabled, user.TwoFactorEnabled); + } + return Task.CompletedTask; + } + + /// + /// Sets the token value for a particular user. + /// + /// The user. + /// The authentication provider for the token. + /// The name of the token. + /// The value of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override async Task SetTokenAsync(TUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var token = await FindTokenAsync(user, loginProvider, name, cancellationToken); + if (token == null) + { + if (user.AddUserToken(CreateUserToken(user, loginProvider, name, value))) + { + MongoRepository.UpdateOne>(user, e => e.Tokens, user.Tokens); + } + //await AddUserTokenAsync(CreateUserToken(user, loginProvider, name, value)); + } + else + { + if (user.SetToken(token, value)) + { + MongoRepository.UpdateOne>(user, e => e.Tokens, user.Tokens); + } + } + } + + /// + /// Deletes a token for a user. + /// + /// The user. + /// The authentication provider for the token. + /// The name of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override async Task RemoveTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var entry = await FindTokenAsync(user, loginProvider, name, cancellationToken); + if (entry != null) + { + if (user.RemoveUserToken(entry)) + { + MongoRepository.UpdateOne>(user, e => e.Tokens, user.Tokens); + } + } + } + + /// + /// Returns the token value. + /// + /// The user. + /// The authentication provider for the token. + /// The name of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override async Task GetTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var entry = await FindTokenAsync(user, loginProvider, name, cancellationToken); + return entry?.Value; + } + + private const string InternalLoginProvider = "[AspNetUserStore]"; + private const string AuthenticatorKeyTokenName = "AuthenticatorKey"; + private const string RecoveryCodeTokenName = "RecoveryCodes"; + + /// + /// Sets the authenticator key for the specified . + /// + /// The user whose authenticator key should be set. + /// The authenticator key to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public override Task SetAuthenticatorKeyAsync(TUser user, string key, CancellationToken cancellationToken) + => SetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, key, cancellationToken); + + /// + /// Get the authenticator key for the specified . + /// + /// The user whose security stamp should be set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the security stamp for the specified . + public override Task GetAuthenticatorKeyAsync(TUser user, CancellationToken cancellationToken) + => GetTokenAsync(user, InternalLoginProvider, AuthenticatorKeyTokenName, cancellationToken); + + /// + /// Returns how many recovery code are still valid for a user. + /// + /// The user who owns the recovery code. + /// The used to propagate notifications that the operation should be canceled. + /// The number of valid recovery codes for the user.. + public override async Task CountCodesAsync(TUser user, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + var mergedCodes = await GetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, cancellationToken) ?? ""; + if (mergedCodes.Length > 0) + { + return mergedCodes.Split(';').Length; + } + return 0; + } + + /// + /// Updates the recovery codes for the user while invalidating any previous recovery codes. + /// + /// The user to store new recovery codes for. + /// The new recovery codes for the user. + /// The used to propagate notifications that the operation should be canceled. + /// The new recovery codes for the user. + public override Task ReplaceCodesAsync(TUser user, IEnumerable recoveryCodes, CancellationToken cancellationToken) + { + var mergedCodes = string.Join(";", recoveryCodes); + return SetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, mergedCodes, cancellationToken); + } + + /// + /// Returns whether a recovery code is valid for a user. Note: recovery codes are only valid + /// once, and will be invalid after use. + /// + /// The user who owns the recovery code. + /// The recovery code to use. + /// The used to propagate notifications that the operation should be canceled. + /// True if the recovery code was found for the user. + public override async Task RedeemCodeAsync(TUser user, string code, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + if (code == null) + { + throw new ArgumentNullException(nameof(code)); + } + + var mergedCodes = await GetTokenAsync(user, InternalLoginProvider, RecoveryCodeTokenName, cancellationToken) ?? ""; + var splitCodes = mergedCodes.Split(';'); + if (splitCodes.Contains(code)) + { + var updatedCodes = new List(splitCodes.Where(s => s != code)); + await ReplaceCodesAsync(user, updatedCodes, cancellationToken); + return true; + } + return false; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Resources.Designer.cs b/src/Resources.Designer.cs new file mode 100644 index 0000000..c31033d --- /dev/null +++ b/src/Resources.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// +// Ce code a été généré par un outil. +// Version du runtime :4.0.30319.42000 +// +// Les modifications apportées à ce fichier peuvent provoquer un comportement incorrect et seront perdues si +// le code est régénéré. +// +//------------------------------------------------------------------------------ + +namespace AspNetCore.Identity.MongoDbCore { + using System; + + + /// + /// Une classe de ressource fortement typée destinée, entre autres, à la consultation des chaînes localisées. + /// + // Cette classe a été générée automatiquement par la classe StronglyTypedResourceBuilder + // à l'aide d'un outil, tel que ResGen ou Visual Studio. + // Pour ajouter ou supprimer un membre, modifiez votre fichier .ResX, puis réexécutez ResGen + // avec l'option /str ou régénérez votre projet VS. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Retourne l'instance ResourceManager mise en cache utilisée par cette classe. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AspNetCore.Identity.MongoDbCore.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Remplace la propriété CurrentUICulture du thread actuel pour toutes + /// les recherches de ressources à l'aide de cette classe de ressource fortement typée. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Recherche une chaîne localisée semblable à AddEntityFrameworkStores can only be called with a role that derives from IdentityRole<TKey, TUserRole, TRoleClaim>.. + /// + internal static string NotIdentityRole { + get { + return ResourceManager.GetString("NotIdentityRole", resourceCulture); + } + } + + /// + /// Recherche une chaîne localisée semblable à AddEntityFrameworkStores can only be called with a user that derives from IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken>.. + /// + internal static string NotIdentityUser { + get { + return ResourceManager.GetString("NotIdentityUser", resourceCulture); + } + } + + /// + /// Recherche une chaîne localisée semblable à Role {0} does not exist.. + /// + internal static string RoleNotFound { + get { + return ResourceManager.GetString("RoleNotFound", resourceCulture); + } + } + + /// + /// Recherche une chaîne localisée semblable à Value cannot be null or empty.. + /// + internal static string ValueCannotBeNullOrEmpty { + get { + return ResourceManager.GetString("ValueCannotBeNullOrEmpty", resourceCulture); + } + } + } +} diff --git a/src/Resources.resx b/src/Resources.resx new file mode 100644 index 0000000..eb19639 --- /dev/null +++ b/src/Resources.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + AddEntityFrameworkStores can only be called with a role that derives from IdentityRole<TKey, TUserRole, TRoleClaim>. + error when the role does not derive from IdentityRole + + + AddEntityFrameworkStores can only be called with a user that derives from IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken>. + error when the user does not derive from IdentityUser + + + Role {0} does not exist. + error when a role does not exist + + + Value cannot be null or empty. + error when something cannot be null or empty + + \ No newline at end of file diff --git a/src/lib/netcoreapp2.0/AspNetCore.Identity.MongoDbCore.deps.json b/src/lib/netcoreapp2.0/AspNetCore.Identity.MongoDbCore.deps.json new file mode 100644 index 0000000..abe1bf5 --- /dev/null +++ b/src/lib/netcoreapp2.0/AspNetCore.Identity.MongoDbCore.deps.json @@ -0,0 +1,1755 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v2.0", + "signature": "bd9370ef5c934f6ee79db0c381cc3742186af303" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v2.0": { + "AspNetCore.Identity.MongoDbCore/1.0.0": { + "dependencies": { + "Microsoft.AspNetCore.Identity": "2.0.0", + "Microsoft.Extensions.Identity.Stores": "2.0.0", + "MongoDB.Driver": "2.4.4", + "MongoDbGenericRepository": "1.3.0" + }, + "runtime": { + "AspNetCore.Identity.MongoDbCore.dll": {} + } + }, + "Microsoft.AspNetCore.Authentication/2.0.0": { + "dependencies": { + "Microsoft.AspNetCore.Authentication.Core": "2.0.0", + "Microsoft.AspNetCore.DataProtection": "2.0.0", + "Microsoft.AspNetCore.Http": "2.0.0", + "Microsoft.AspNetCore.Http.Extensions": "2.0.0", + "Microsoft.Extensions.Logging.Abstractions": "2.0.0", + "Microsoft.Extensions.Options": "2.0.0", + "Microsoft.Extensions.WebEncoders": "2.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.Authentication.dll": {} + } + }, + "Microsoft.AspNetCore.Authentication.Abstractions/2.0.0": { + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.0.0", + "Microsoft.Extensions.Logging.Abstractions": "2.0.0", + "Microsoft.Extensions.Options": "2.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.Authentication.Abstractions.dll": {} + } + }, + "Microsoft.AspNetCore.Authentication.Cookies/2.0.0": { + "dependencies": { + "Microsoft.AspNetCore.Authentication": "2.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.Authentication.Cookies.dll": {} + } + }, + "Microsoft.AspNetCore.Authentication.Core/2.0.0": { + "dependencies": { + "Microsoft.AspNetCore.Authentication.Abstractions": "2.0.0", + "Microsoft.AspNetCore.Http": "2.0.0", + "Microsoft.AspNetCore.Http.Extensions": "2.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.Authentication.Core.dll": {} + } + }, + "Microsoft.AspNetCore.Cryptography.Internal/2.0.0": { + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.Cryptography.Internal.dll": {} + } + }, + "Microsoft.AspNetCore.Cryptography.KeyDerivation/2.0.0": { + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "2.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.Cryptography.KeyDerivation.dll": {} + } + }, + "Microsoft.AspNetCore.DataProtection/2.0.0": { + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "2.0.0", + "Microsoft.AspNetCore.DataProtection.Abstractions": "2.0.0", + "Microsoft.AspNetCore.Hosting.Abstractions": "2.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.0.0", + "Microsoft.Extensions.Logging.Abstractions": "2.0.0", + "Microsoft.Extensions.Options": "2.0.0", + "Microsoft.Win32.Registry": "4.4.0", + "System.Security.Cryptography.Xml": "4.4.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.DataProtection.dll": {} + } + }, + "Microsoft.AspNetCore.DataProtection.Abstractions/2.0.0": { + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.DataProtection.Abstractions.dll": {} + } + }, + "Microsoft.AspNetCore.Hosting.Abstractions/2.0.0": { + "dependencies": { + "Microsoft.AspNetCore.Hosting.Server.Abstractions": "2.0.0", + "Microsoft.AspNetCore.Http.Abstractions": "2.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "2.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "2.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "2.0.0", + "Microsoft.Extensions.Logging.Abstractions": "2.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.Hosting.Abstractions.dll": {} + } + }, + "Microsoft.AspNetCore.Hosting.Server.Abstractions/2.0.0": { + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "2.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.Hosting.Server.Abstractions.dll": {} + } + }, + "Microsoft.AspNetCore.Http/2.0.0": { + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.0.0", + "Microsoft.AspNetCore.WebUtilities": "2.0.0", + "Microsoft.Extensions.ObjectPool": "2.0.0", + "Microsoft.Extensions.Options": "2.0.0", + "Microsoft.Net.Http.Headers": "2.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.Http.dll": {} + } + }, + "Microsoft.AspNetCore.Http.Abstractions/2.0.0": { + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.0.0", + "System.Text.Encodings.Web": "4.4.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.Http.Abstractions.dll": {} + } + }, + "Microsoft.AspNetCore.Http.Extensions/2.0.0": { + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "2.0.0", + "Microsoft.Net.Http.Headers": "2.0.0", + "System.Buffers": "4.4.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.Http.Extensions.dll": {} + } + }, + "Microsoft.AspNetCore.Http.Features/2.0.0": { + "dependencies": { + "Microsoft.Extensions.Primitives": "2.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.Http.Features.dll": {} + } + }, + "Microsoft.AspNetCore.Identity/2.0.0": { + "dependencies": { + "Microsoft.AspNetCore.Authentication.Cookies": "2.0.0", + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "2.0.0", + "Microsoft.AspNetCore.Hosting.Abstractions": "2.0.0", + "Microsoft.Extensions.Identity.Core": "2.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.Identity.dll": {} + } + }, + "Microsoft.AspNetCore.WebUtilities/2.0.0": { + "dependencies": { + "Microsoft.Net.Http.Headers": "2.0.0", + "System.Text.Encodings.Web": "4.4.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.AspNetCore.WebUtilities.dll": {} + } + }, + "Microsoft.Extensions.Configuration.Abstractions/2.0.0": { + "dependencies": { + "Microsoft.Extensions.Primitives": "2.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.Extensions.Configuration.Abstractions.dll": {} + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions/2.0.0": { + "runtime": { + "lib/netstandard2.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {} + } + }, + "Microsoft.Extensions.FileProviders.Abstractions/2.0.0": { + "dependencies": { + "Microsoft.Extensions.Primitives": "2.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.Extensions.FileProviders.Abstractions.dll": {} + } + }, + "Microsoft.Extensions.Hosting.Abstractions/2.0.0": { + "runtime": { + "lib/netstandard2.0/Microsoft.Extensions.Hosting.Abstractions.dll": {} + } + }, + "Microsoft.Extensions.Identity.Core/2.0.0": { + "dependencies": { + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "2.0.0", + "Microsoft.Extensions.Logging": "2.0.0", + "Microsoft.Extensions.Options": "2.0.0", + "System.ComponentModel.Annotations": "4.4.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.Extensions.Identity.Core.dll": {} + } + }, + "Microsoft.Extensions.Identity.Stores/2.0.0": { + "dependencies": { + "Microsoft.Extensions.Identity.Core": "2.0.0", + "Microsoft.Extensions.Logging": "2.0.0", + "System.ComponentModel.Annotations": "4.4.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.Extensions.Identity.Stores.dll": {} + } + }, + "Microsoft.Extensions.Logging/2.0.0": { + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.0.0", + "Microsoft.Extensions.Logging.Abstractions": "2.0.0", + "Microsoft.Extensions.Options": "2.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.Extensions.Logging.dll": {} + } + }, + "Microsoft.Extensions.Logging.Abstractions/2.0.0": { + "runtime": { + "lib/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.dll": {} + } + }, + "Microsoft.Extensions.ObjectPool/2.0.0": { + "runtime": { + "lib/netstandard2.0/Microsoft.Extensions.ObjectPool.dll": {} + } + }, + "Microsoft.Extensions.Options/2.0.0": { + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.0.0", + "Microsoft.Extensions.Primitives": "2.0.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.Extensions.Options.dll": {} + } + }, + "Microsoft.Extensions.Primitives/2.0.0": { + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.4.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.Extensions.Primitives.dll": {} + } + }, + "Microsoft.Extensions.WebEncoders/2.0.0": { + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.0.0", + "Microsoft.Extensions.Options": "2.0.0", + "System.Text.Encodings.Web": "4.4.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.Extensions.WebEncoders.dll": {} + } + }, + "Microsoft.Net.Http.Headers/2.0.0": { + "dependencies": { + "Microsoft.Extensions.Primitives": "2.0.0", + "System.Buffers": "4.4.0" + }, + "runtime": { + "lib/netstandard2.0/Microsoft.Net.Http.Headers.dll": {} + } + }, + "Microsoft.NETCore.Targets/1.0.1": {}, + "Microsoft.Win32.Primitives/4.0.1": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "Microsoft.Win32.Registry/4.4.0": { + "dependencies": { + "System.Security.AccessControl": "4.4.0", + "System.Security.Principal.Windows": "4.4.0" + }, + "runtimeTargets": { + "runtime/unix/lib/_._": { + "rid": "unix", + "assetType": "runtime" + }, + "runtime/win/lib/_._": { + "rid": "win", + "assetType": "runtime" + } + } + }, + "MongoDB.Bson/2.4.4": { + "dependencies": { + "System.Collections.NonGeneric": "4.0.1", + "System.Diagnostics.Process": "4.1.0", + "System.Dynamic.Runtime": "4.0.11", + "System.Reflection.Emit.Lightweight": "4.0.1" + }, + "runtime": { + "lib/netstandard1.5/MongoDB.Bson.dll": {} + } + }, + "MongoDB.Driver/2.4.4": { + "dependencies": { + "MongoDB.Bson": "2.4.4", + "MongoDB.Driver.Core": "2.4.4", + "System.Linq.Queryable": "4.0.1" + }, + "runtime": { + "lib/netstandard1.5/MongoDB.Driver.dll": {} + } + }, + "MongoDB.Driver.Core/2.4.4": { + "dependencies": { + "MongoDB.Bson": "2.4.4", + "System.Collections.Specialized": "4.0.1", + "System.Diagnostics.TraceSource": "4.0.0", + "System.Net.NameResolution": "4.0.0", + "System.Net.Security": "4.0.0", + "System.Security.SecureString": "4.0.0" + }, + "runtime": { + "lib/netstandard1.5/MongoDB.Driver.Core.dll": {} + } + }, + "MongoDbGenericRepository/1.3.0": { + "runtime": { + "lib/netstandard2.0/MongoDbGenericRepository.dll": {} + } + }, + "runtime.native.System/4.0.0": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "runtime.native.System.Net.Http/4.0.1": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "runtime.native.System.Net.Security/4.0.1": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "runtime.native.System.Security.Cryptography/4.0.0": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "System.Buffers/4.4.0": {}, + "System.Collections/4.0.11": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Collections.Concurrent/4.0.12": { + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Diagnostics.Tracing": "4.1.0", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11", + "System.Threading.Tasks": "4.0.11" + } + }, + "System.Collections.NonGeneric/4.0.1": { + "dependencies": { + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "System.Collections.Specialized/4.0.1": { + "dependencies": { + "System.Collections.NonGeneric": "4.0.1", + "System.Globalization": "4.0.11", + "System.Globalization.Extensions": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "System.ComponentModel.Annotations/4.4.0": {}, + "System.Diagnostics.Debug/4.0.11": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Diagnostics.Process/4.1.0": { + "dependencies": { + "Microsoft.Win32.Primitives": "4.0.1", + "Microsoft.Win32.Registry": "4.4.0", + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.IO.FileSystem": "4.0.1", + "System.IO.FileSystem.Primitives": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Text.Encoding.Extensions": "4.0.11", + "System.Threading": "4.0.11", + "System.Threading.Tasks": "4.0.11", + "System.Threading.Thread": "4.0.0", + "System.Threading.ThreadPool": "4.0.10", + "runtime.native.System": "4.0.0" + }, + "runtimeTargets": { + "runtime/linux/lib/_._": { + "rid": "linux", + "assetType": "runtime" + }, + "runtime/osx/lib/_._": { + "rid": "osx", + "assetType": "runtime" + }, + "runtime/win/lib/_._": { + "rid": "win", + "assetType": "runtime" + } + } + }, + "System.Diagnostics.TraceSource/4.0.0": { + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11", + "runtime.native.System": "4.0.0" + }, + "runtimeTargets": { + "runtime/unix/lib/_._": { + "rid": "unix", + "assetType": "runtime" + }, + "runtime/win/lib/_._": { + "rid": "win", + "assetType": "runtime" + } + } + }, + "System.Diagnostics.Tracing/4.1.0": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Dynamic.Runtime/4.0.11": { + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.Linq": "4.1.0", + "System.Linq.Expressions": "4.1.0", + "System.ObjectModel": "4.0.12", + "System.Reflection": "4.1.0", + "System.Reflection.Emit": "4.0.1", + "System.Reflection.Emit.ILGeneration": "4.0.1", + "System.Reflection.Primitives": "4.0.1", + "System.Reflection.TypeExtensions": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "System.Globalization/4.0.11": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Globalization.Calendars/4.0.1": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Runtime": "4.1.0" + } + }, + "System.Globalization.Extensions/4.0.1": { + "dependencies": { + "System.Globalization": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.InteropServices": "4.1.0" + }, + "runtimeTargets": { + "runtime/unix/lib/_._": { + "rid": "unix", + "assetType": "runtime" + }, + "runtime/win/lib/_._": { + "rid": "win", + "assetType": "runtime" + } + } + }, + "System.IO/4.1.0": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Threading.Tasks": "4.0.11" + } + }, + "System.IO.FileSystem/4.0.1": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.IO": "4.1.0", + "System.IO.FileSystem.Primitives": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Text.Encoding": "4.0.11", + "System.Threading.Tasks": "4.0.11" + } + }, + "System.IO.FileSystem.Primitives/4.0.1": { + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "System.Linq/4.1.0": { + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0" + } + }, + "System.Linq.Expressions/4.1.0": { + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Linq": "4.1.0", + "System.ObjectModel": "4.0.12", + "System.Reflection": "4.1.0", + "System.Reflection.Emit": "4.0.1", + "System.Reflection.Emit.ILGeneration": "4.0.1", + "System.Reflection.Emit.Lightweight": "4.0.1", + "System.Reflection.Extensions": "4.0.1", + "System.Reflection.Primitives": "4.0.1", + "System.Reflection.TypeExtensions": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "System.Linq.Queryable/4.0.1": { + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Linq": "4.1.0", + "System.Linq.Expressions": "4.1.0", + "System.Reflection": "4.1.0", + "System.Reflection.Extensions": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Net.NameResolution/4.0.0": { + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Tracing": "4.1.0", + "System.Globalization": "4.0.11", + "System.Net.Primitives": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", + "System.Security.Principal.Windows": "4.4.0", + "System.Threading": "4.0.11", + "System.Threading.Tasks": "4.0.11", + "runtime.native.System": "4.0.0" + }, + "runtimeTargets": { + "runtime/unix/lib/_._": { + "rid": "unix", + "assetType": "runtime" + }, + "runtime/win/lib/_._": { + "rid": "win", + "assetType": "runtime" + } + } + }, + "System.Net.Primitives/4.0.11": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Handles": "4.0.1" + } + }, + "System.Net.Security/4.0.0": { + "dependencies": { + "Microsoft.Win32.Primitives": "4.0.1", + "System.Collections": "4.0.11", + "System.Collections.Concurrent": "4.0.12", + "System.Diagnostics.Tracing": "4.1.0", + "System.Globalization": "4.0.11", + "System.Globalization.Extensions": "4.0.1", + "System.IO": "4.1.0", + "System.Net.Primitives": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", + "System.Security.Claims": "4.0.1", + "System.Security.Cryptography.Algorithms": "4.2.0", + "System.Security.Cryptography.Encoding": "4.0.0", + "System.Security.Cryptography.OpenSsl": "4.0.0", + "System.Security.Cryptography.Primitives": "4.0.0", + "System.Security.Cryptography.X509Certificates": "4.1.0", + "System.Security.Principal": "4.0.1", + "System.Text.Encoding": "4.0.11", + "System.Threading": "4.0.11", + "System.Threading.Tasks": "4.0.11", + "System.Threading.ThreadPool": "4.0.10", + "runtime.native.System": "4.0.0", + "runtime.native.System.Net.Security": "4.0.1", + "runtime.native.System.Security.Cryptography": "4.0.0" + }, + "runtimeTargets": { + "runtime/unix/lib/_._": { + "rid": "unix", + "assetType": "runtime" + }, + "runtime/win/lib/_._": { + "rid": "win", + "assetType": "runtime" + } + } + }, + "System.ObjectModel/4.0.12": { + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "System.Reflection/4.1.0": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.IO": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Reflection.Emit/4.0.1": { + "dependencies": { + "System.IO": "4.1.0", + "System.Reflection": "4.1.0", + "System.Reflection.Emit.ILGeneration": "4.0.1", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Reflection.Emit.ILGeneration/4.0.1": { + "dependencies": { + "System.Reflection": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Reflection.Emit.Lightweight/4.0.1": { + "dependencies": { + "System.Reflection": "4.1.0", + "System.Reflection.Emit.ILGeneration": "4.0.1", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Reflection.Extensions/4.0.1": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0" + } + }, + "System.Reflection.Primitives/4.0.1": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Reflection.TypeExtensions/4.1.0": { + "dependencies": { + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0" + } + }, + "System.Resources.ResourceManager/4.0.1": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0" + } + }, + "System.Runtime/4.1.0": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "System.Runtime.CompilerServices.Unsafe/4.4.0": { + "runtime": { + "lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll": {} + } + }, + "System.Runtime.Extensions/4.1.0": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Runtime.Handles/4.0.1": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Runtime.InteropServices/4.1.0": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Reflection": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Handles": "4.0.1" + } + }, + "System.Runtime.Numerics/4.0.1": { + "dependencies": { + "System.Globalization": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0" + } + }, + "System.Security.AccessControl/4.4.0": { + "dependencies": { + "System.Security.Principal.Windows": "4.4.0" + }, + "runtimeTargets": { + "runtime/unix/lib/_._": { + "rid": "unix", + "assetType": "runtime" + }, + "runtime/win/lib/_._": { + "rid": "win", + "assetType": "runtime" + } + } + }, + "System.Security.Claims/4.0.1": { + "dependencies": { + "System.Collections": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Security.Principal": "4.0.1" + } + }, + "System.Security.Cryptography.Algorithms/4.2.0": { + "dependencies": { + "System.Collections": "4.0.11", + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", + "System.Runtime.Numerics": "4.0.1", + "System.Security.Cryptography.Encoding": "4.0.0", + "System.Security.Cryptography.Primitives": "4.0.0", + "System.Text.Encoding": "4.0.11", + "runtime.native.System.Security.Cryptography": "4.0.0" + }, + "runtimeTargets": { + "runtime/unix/lib/_._": { + "rid": "unix", + "assetType": "runtime" + }, + "runtime/win/lib/_._": { + "rid": "win", + "assetType": "runtime" + } + } + }, + "System.Security.Cryptography.Cng/4.2.0": { + "dependencies": { + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", + "System.Security.Cryptography.Algorithms": "4.2.0", + "System.Security.Cryptography.Encoding": "4.0.0", + "System.Security.Cryptography.Primitives": "4.0.0", + "System.Text.Encoding": "4.0.11" + }, + "runtimeTargets": { + "runtime/unix/lib/_._": { + "rid": "unix", + "assetType": "runtime" + }, + "runtime/win/lib/_._": { + "rid": "win", + "assetType": "runtime" + } + } + }, + "System.Security.Cryptography.Csp/4.0.0": { + "dependencies": { + "System.IO": "4.1.0", + "System.Reflection": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", + "System.Security.Cryptography.Algorithms": "4.2.0", + "System.Security.Cryptography.Encoding": "4.0.0", + "System.Security.Cryptography.Primitives": "4.0.0", + "System.Text.Encoding": "4.0.11", + "System.Threading": "4.0.11" + }, + "runtimeTargets": { + "runtime/unix/lib/_._": { + "rid": "unix", + "assetType": "runtime" + }, + "runtime/win/lib/_._": { + "rid": "win", + "assetType": "runtime" + } + } + }, + "System.Security.Cryptography.Encoding/4.0.0": { + "dependencies": { + "System.Collections": "4.0.11", + "System.Collections.Concurrent": "4.0.12", + "System.Linq": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", + "System.Security.Cryptography.Primitives": "4.0.0", + "System.Text.Encoding": "4.0.11", + "runtime.native.System.Security.Cryptography": "4.0.0" + }, + "runtimeTargets": { + "runtime/unix/lib/_._": { + "rid": "unix", + "assetType": "runtime" + }, + "runtime/win/lib/_._": { + "rid": "win", + "assetType": "runtime" + } + } + }, + "System.Security.Cryptography.OpenSsl/4.0.0": { + "dependencies": { + "System.Collections": "4.0.11", + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", + "System.Runtime.Numerics": "4.0.1", + "System.Security.Cryptography.Algorithms": "4.2.0", + "System.Security.Cryptography.Encoding": "4.0.0", + "System.Security.Cryptography.Primitives": "4.0.0", + "System.Text.Encoding": "4.0.11", + "runtime.native.System.Security.Cryptography": "4.0.0" + }, + "runtimeTargets": { + "runtime/unix/lib/_._": { + "rid": "unix", + "assetType": "runtime" + } + } + }, + "System.Security.Cryptography.Primitives/4.0.0": { + "dependencies": { + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Threading": "4.0.11", + "System.Threading.Tasks": "4.0.11" + } + }, + "System.Security.Cryptography.X509Certificates/4.1.0": { + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.Globalization.Calendars": "4.0.1", + "System.IO": "4.1.0", + "System.IO.FileSystem": "4.0.1", + "System.IO.FileSystem.Primitives": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", + "System.Runtime.Numerics": "4.0.1", + "System.Security.Cryptography.Algorithms": "4.2.0", + "System.Security.Cryptography.Cng": "4.2.0", + "System.Security.Cryptography.Csp": "4.0.0", + "System.Security.Cryptography.Encoding": "4.0.0", + "System.Security.Cryptography.OpenSsl": "4.0.0", + "System.Security.Cryptography.Primitives": "4.0.0", + "System.Text.Encoding": "4.0.11", + "System.Threading": "4.0.11", + "runtime.native.System": "4.0.0", + "runtime.native.System.Net.Http": "4.0.1", + "runtime.native.System.Security.Cryptography": "4.0.0" + }, + "runtimeTargets": { + "runtime/unix/lib/_._": { + "rid": "unix", + "assetType": "runtime" + }, + "runtime/win/lib/_._": { + "rid": "win", + "assetType": "runtime" + } + } + }, + "System.Security.Cryptography.Xml/4.4.0": { + "runtime": { + "lib/netstandard2.0/System.Security.Cryptography.Xml.dll": {} + } + }, + "System.Security.Principal/4.0.1": { + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "System.Security.Principal.Windows/4.4.0": { + "runtimeTargets": { + "runtime/unix/lib/_._": { + "rid": "unix", + "assetType": "runtime" + }, + "runtime/win/lib/_._": { + "rid": "win", + "assetType": "runtime" + } + } + }, + "System.Security.SecureString/4.0.0": { + "dependencies": { + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "System.Runtime.InteropServices": "4.1.0", + "System.Security.Cryptography.Primitives": "4.0.0", + "System.Text.Encoding": "4.0.11", + "System.Threading": "4.0.11" + }, + "runtimeTargets": { + "runtime/unix/lib/_._": { + "rid": "unix", + "assetType": "runtime" + }, + "runtime/win/lib/_._": { + "rid": "win", + "assetType": "runtime" + } + } + }, + "System.Text.Encoding/4.0.11": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Text.Encoding.Extensions/4.0.11": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Text.Encoding": "4.0.11" + } + }, + "System.Text.Encodings.Web/4.4.0": { + "runtime": { + "lib/netstandard2.0/System.Text.Encodings.Web.dll": {} + } + }, + "System.Threading/4.0.11": { + "dependencies": { + "System.Runtime": "4.1.0", + "System.Threading.Tasks": "4.0.11" + } + }, + "System.Threading.Tasks/4.0.11": { + "dependencies": { + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Threading.Thread/4.0.0": { + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "System.Threading.ThreadPool/4.0.10": { + "dependencies": { + "System.Runtime": "4.1.0", + "System.Runtime.Handles": "4.0.1" + } + } + } + }, + "libraries": { + "AspNetCore.Identity.MongoDbCore/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Microsoft.AspNetCore.Authentication/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-QmgG+EkJg+lObB7XffZGrbj1E8JE74ZePyVsuywhXrvPDObrjUwyvsbkOQvdtjiULhkh+8fgfhBcDTTiMr56Eg==", + "path": "microsoft.aspnetcore.authentication/2.0.0", + "hashPath": "microsoft.aspnetcore.authentication.2.0.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.Authentication.Abstractions/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-eDrNQlYPL5lw8DXMlEvo61VhkZ9DFq9/8Fds8+aaMa4nMtIJvwEwbFyYfJ+Zblh/8NNBkDQHkDD1p4i3g0HFWg==", + "path": "microsoft.aspnetcore.authentication.abstractions/2.0.0", + "hashPath": "microsoft.aspnetcore.authentication.abstractions.2.0.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.Authentication.Cookies/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-330xWVaI3isU9h0oaZB7CByB93F60CSstbSm4XnPObpJHXkBrW3gth+ujn0oHGyRoP7Fo1UK/kROc8gvymFGTw==", + "path": "microsoft.aspnetcore.authentication.cookies/2.0.0", + "hashPath": "microsoft.aspnetcore.authentication.cookies.2.0.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.Authentication.Core/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-fO3HRV8+8trdBvi0zpQBu/TtoK//JC4fdeREud08589wxc8+mkP9gzXuLMMst88fa5EkjPeIGEnc2OvRpOLyMw==", + "path": "microsoft.aspnetcore.authentication.core/2.0.0", + "hashPath": "microsoft.aspnetcore.authentication.core.2.0.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.Cryptography.Internal/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-SY6GQyZZ5o09rqFmy3nhyJzx3lkFDBl0wO2Kb7EoLCPyH6dC7KB+QXysHfa9P5jHPiYB9VEkcQ9H7kQKcXQ1sw==", + "path": "microsoft.aspnetcore.cryptography.internal/2.0.0", + "hashPath": "microsoft.aspnetcore.cryptography.internal.2.0.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.Cryptography.KeyDerivation/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-kXszvwovn6Xot8JvRVf5KL9HXHzVVirs9diPkzMDNoPWMvubXRisw1d3T2ETFCgx2MOOhfUu5+LXlybC1ITkOQ==", + "path": "microsoft.aspnetcore.cryptography.keyderivation/2.0.0", + "hashPath": "microsoft.aspnetcore.cryptography.keyderivation.2.0.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.DataProtection/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-CjRLA26BpKrzBqpw1g9F3rGYNGisPd+zsnYdpJbHsjH4iIbi/OHfgKzGdHZCwmfQWrlL4e8Q0SpS+DMvgf6Jpg==", + "path": "microsoft.aspnetcore.dataprotection/2.0.0", + "hashPath": "microsoft.aspnetcore.dataprotection.2.0.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.DataProtection.Abstractions/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-BiFPWLZTKw253oQ5lAXcCkFkNFSRNi8fDCUB2yOTQyuYVMR8pnBAhVJ37o/E6bnuFYrE6eFCU4iDYrShmBIBYA==", + "path": "microsoft.aspnetcore.dataprotection.abstractions/2.0.0", + "hashPath": "microsoft.aspnetcore.dataprotection.abstractions.2.0.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.Hosting.Abstractions/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-IR2zlm3d/CmYbkw+cMM7M6mUAi+xsFUPfWqGYqzZVC5o6jX3xD2Z4Uf44UBaWKMBf5Z7q9dodIdXxwFPF2Hxhg==", + "path": "microsoft.aspnetcore.hosting.abstractions/2.0.0", + "hashPath": "microsoft.aspnetcore.hosting.abstractions.2.0.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.Hosting.Server.Abstractions/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-v2H65ix/O11HKoxhKQpljtozsD5/1tqeXr3TYnrLgfAPIsp6kTFxIcTSENoxtew7h9X14ENqUf2lBCkyCNRUuQ==", + "path": "microsoft.aspnetcore.hosting.server.abstractions/2.0.0", + "hashPath": "microsoft.aspnetcore.hosting.server.abstractions.2.0.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.Http/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-2YNhcHrGxo2YufA8TYGyaEMIJwikyisZqEzHCRpIuI0D6ZXkA47U/3NJg2r/x5/gGHNM3TXO7DsqH88qRda+yg==", + "path": "microsoft.aspnetcore.http/2.0.0", + "hashPath": "microsoft.aspnetcore.http.2.0.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.Http.Abstractions/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-pblZLY7IfNqhQ5wwGQ0vNq2mG6W5YgZI1fk7suEuwZsGxGEADNBAyNlTALM9L8nMXdvbp6aHP/t4wHrFpcL3Sw==", + "path": "microsoft.aspnetcore.http.abstractions/2.0.0", + "hashPath": "microsoft.aspnetcore.http.abstractions.2.0.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.Http.Extensions/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-lA7Bwvur19MhXrlW0w+WBXONJMSFYY5kNazflz4MNwMZMtzwHxNA6fC5sQsssYd/XvA0gMyKwp52s68uuKLR1w==", + "path": "microsoft.aspnetcore.http.extensions/2.0.0", + "hashPath": "microsoft.aspnetcore.http.extensions.2.0.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.Http.Features/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-yk62muzFTZTKCQuo3nmVPkPvGBlM2qbdSxbX62TufuONuKQrTGQ/SwhwBbYutk5/YY7u4HETu0n9BKOn7mMgmA==", + "path": "microsoft.aspnetcore.http.features/2.0.0", + "hashPath": "microsoft.aspnetcore.http.features.2.0.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.Identity/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Ax5mfrcZRI/1nGvHsi3mA9EvxCkleFnlmhd+ZMJ4PYHTHtL1o51vCMInYCd5oGuSCkRgXHn0U9bAfuQPpaCdvg==", + "path": "microsoft.aspnetcore.identity/2.0.0", + "hashPath": "microsoft.aspnetcore.identity.2.0.0.nupkg.sha512" + }, + "Microsoft.AspNetCore.WebUtilities/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-RqDEwy7jdHJ0NunWydSzJrpODnsF7NPdB0KaRdG60H1bMEt4DbjcWkUb+XxjZ15uWCMi7clTQClpPuIFLwD1yQ==", + "path": "microsoft.aspnetcore.webutilities/2.0.0", + "hashPath": "microsoft.aspnetcore.webutilities.2.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.Configuration.Abstractions/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-rHFrXqMIvQNq51H8RYTO4IWmDOYh8NUzyqGlh0xHWTP6XYnKk7Ryinys2uDs+Vu88b3AMlM3gBBSs78m6OQpYQ==", + "path": "microsoft.extensions.configuration.abstractions/2.0.0", + "hashPath": "microsoft.extensions.configuration.abstractions.2.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.DependencyInjection.Abstractions/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-eUdJ0Q/GfVyUJc0Jal5L1QZLceL78pvEM9wEKcHeI24KorqMDoVX+gWsMGLulQMfOwsUaPtkpQM2pFERTzSfSg==", + "path": "microsoft.extensions.dependencyinjection.abstractions/2.0.0", + "hashPath": "microsoft.extensions.dependencyinjection.abstractions.2.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.FileProviders.Abstractions/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Z0AK+hmLO33WAXQ5P1uPzhH7z5yjDHX/XnUefXxE//SyvCb9x4cVjND24dT5566t/yzGp8/WLD7EG9KQKZZklQ==", + "path": "microsoft.extensions.fileproviders.abstractions/2.0.0", + "hashPath": "microsoft.extensions.fileproviders.abstractions.2.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.Hosting.Abstractions/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-qPG6Ip/AdHxMJ7j3z8FkkpCbV8yjtiFpf/aOpN3TwfJWbtYpN+BKV8Q+pqPMgk7XZivcju9yARaEVCS++hWopA==", + "path": "microsoft.extensions.hosting.abstractions/2.0.0", + "hashPath": "microsoft.extensions.hosting.abstractions.2.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.Identity.Core/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-67UFFAYU/kiJdUgkV3zCx/4yonZ62TI+fP2xXebbSbGjQ2vHHVcNdmP+kX0mHcUfDNRgA+5NBpO/DxiFq0kZFA==", + "path": "microsoft.extensions.identity.core/2.0.0", + "hashPath": "microsoft.extensions.identity.core.2.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.Identity.Stores/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-1q2oszwyzJWQY6yaHfKpXX/1rKbLKkQH/qgmQtvf7PQ2QhS3wGn4/RXKjp/flcOr3j2FmWzdyH0+Y5YGRfI8Iw==", + "path": "microsoft.extensions.identity.stores/2.0.0", + "hashPath": "microsoft.extensions.identity.stores.2.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.Logging/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-VP10syWV/vxYYMKgZ2eDESmUsz3gPxvBn5J6tkVN8lI4M+dF43RN8fWsEPbcAneDmZrHl3Pv23z05nmyGkJlpg==", + "path": "microsoft.extensions.logging/2.0.0", + "hashPath": "microsoft.extensions.logging.2.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.Logging.Abstractions/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-6ZCllUYGFukkymSTx3Yr0G/ajRxoNJp7/FqSxSB4fGISST54ifBhgu4Nc0ItGi3i6DqwuNd8SUyObmiC++AO2Q==", + "path": "microsoft.extensions.logging.abstractions/2.0.0", + "hashPath": "microsoft.extensions.logging.abstractions.2.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.ObjectPool/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-drOmgNZCJiNEqFM/TvyqwtogS8wqoWGQCW5KB/CVGKL6VXHw8OOMdaHyspp8HPstP9UDnrnuq+8eaCaAcQg6tA==", + "path": "microsoft.extensions.objectpool/2.0.0", + "hashPath": "microsoft.extensions.objectpool.2.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.Options/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-sAKBgjl2gWsECBLLR9K54T7/uZaP2n9GhMYHay/oOLfvpvX0+iNAlQ2NJgVE352C9Fs5CDV3VbNTK8T2aNKQFA==", + "path": "microsoft.extensions.options/2.0.0", + "hashPath": "microsoft.extensions.options.2.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.Primitives/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-ukg53qNlqTrK38WA30b5qhw0GD7y3jdI9PHHASjdKyTcBHTevFM2o23tyk3pWCgAV27Bbkm+CPQ2zUe1ZOuYSA==", + "path": "microsoft.extensions.primitives/2.0.0", + "hashPath": "microsoft.extensions.primitives.2.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.WebEncoders/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-5lXmAmfMaVssZwruaPM5hgk7QfzL1dfAaPEw9Ex24wt/D3EPRt7kOqsZoJP3IhVBoccjsTj8DsFJHtQ8bZIFkA==", + "path": "microsoft.extensions.webencoders/2.0.0", + "hashPath": "microsoft.extensions.webencoders.2.0.0.nupkg.sha512" + }, + "Microsoft.Net.Http.Headers/2.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Rm9zeNCWyNrGnysHdRXJpNfeDVlPzzFuidSuRLRNvOrnw71vgNPlR4H9wHo2hG/oSaruukqNjK06MDQqb+eXhA==", + "path": "microsoft.net.http.headers/2.0.0", + "hashPath": "microsoft.net.http.headers.2.0.0.nupkg.sha512" + }, + "Microsoft.NETCore.Targets/1.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-rkn+fKobF/cbWfnnfBOQHKVKIOpxMZBvlSHkqDWgBpwGDcLRduvs3D9OLGeV6GWGvVwNlVi2CBbTjuPmtHvyNw==", + "path": "microsoft.netcore.targets/1.0.1", + "hashPath": "microsoft.netcore.targets.1.0.1.nupkg.sha512" + }, + "Microsoft.Win32.Primitives/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-fQnBHO9DgcmkC9dYSJoBqo6sH1VJwJprUHh8F3hbcRlxiQiBUuTntdk8tUwV490OqC2kQUrinGwZyQHTieuXRA==", + "path": "microsoft.win32.primitives/4.0.1", + "hashPath": "microsoft.win32.primitives.4.0.1.nupkg.sha512" + }, + "Microsoft.Win32.Registry/4.4.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-dA36TlNVn/XfrZtmf0fiI/z1nd3Wfp2QVzTdj26pqgP9LFWq0i1hYEUAW50xUjGFYn1+/cP3KGuxT2Yn1OUNBQ==", + "path": "microsoft.win32.registry/4.4.0", + "hashPath": "microsoft.win32.registry.4.4.0.nupkg.sha512" + }, + "MongoDB.Bson/2.4.4": { + "type": "package", + "serviceable": true, + "sha512": "sha512-BavFx+rmR5k+dx14tC23KTyjCPkASvTQ1WxzLYHt2w3Mkqel5lJV6+gWzWV3DX9hnTewCC09OKqNqckiJl4sIw==", + "path": "mongodb.bson/2.4.4", + "hashPath": "mongodb.bson.2.4.4.nupkg.sha512" + }, + "MongoDB.Driver/2.4.4": { + "type": "package", + "serviceable": true, + "sha512": "sha512-sG+4H7732fG3XGCXWsxwuUQBvnbVO/bzzxBVZHtHa5R2UDsRXR7BfQxAS/d9Qk8FlNDHOjTjz+GzWTgzjgopQw==", + "path": "mongodb.driver/2.4.4", + "hashPath": "mongodb.driver.2.4.4.nupkg.sha512" + }, + "MongoDB.Driver.Core/2.4.4": { + "type": "package", + "serviceable": true, + "sha512": "sha512-fVjXuQE5Qe2P38xz9wz5V0QhT54+ZT78/JUKMMbIXOKYVFgkzEOE7UU6ZsbC/AbR4lwGIpRQZoiv7wW3rJb3xQ==", + "path": "mongodb.driver.core/2.4.4", + "hashPath": "mongodb.driver.core.2.4.4.nupkg.sha512" + }, + "MongoDbGenericRepository/1.3.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-PwRYZ+htcg6gpKHfQfoQcVXdoT4sYyroQVnooTG+gcxTwzI7HOwyxkUQhLPoWcSLSo2QGFkAJfwhtqE0JgBxOA==", + "path": "mongodbgenericrepository/1.3.0", + "hashPath": "mongodbgenericrepository.1.3.0.nupkg.sha512" + }, + "runtime.native.System/4.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-QfS/nQI7k/BLgmLrw7qm7YBoULEvgWnPI+cYsbfCVFTW8Aj+i8JhccxcFMu1RWms0YZzF+UHguNBK4Qn89e2Sg==", + "path": "runtime.native.system/4.0.0", + "hashPath": "runtime.native.system.4.0.0.nupkg.sha512" + }, + "runtime.native.System.Net.Http/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Nh0UPZx2Vifh8r+J+H2jxifZUD3sBrmolgiFWJd2yiNrxO0xTa6bAw3YwRn1VOiSen/tUXMS31ttNItCZ6lKuA==", + "path": "runtime.native.system.net.http/4.0.1", + "hashPath": "runtime.native.system.net.http.4.0.1.nupkg.sha512" + }, + "runtime.native.System.Net.Security/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Az6Ff6rZFb8nYGAaejFR6jr8ktt9f3e1Q/yKdw0pwHNTLaO/1eCAC9vzBoR9YAb0QeZD6fZXl1A9tRB5stpzXA==", + "path": "runtime.native.system.net.security/4.0.1", + "hashPath": "runtime.native.system.net.security.4.0.1.nupkg.sha512" + }, + "runtime.native.System.Security.Cryptography/4.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-2CQK0jmO6Eu7ZeMgD+LOFbNJSXHFVQbCJJkEyEwowh1SCgYnrn9W9RykMfpeeVGw7h4IBvYikzpGUlmZTUafJw==", + "path": "runtime.native.system.security.cryptography/4.0.0", + "hashPath": "runtime.native.system.security.cryptography.4.0.0.nupkg.sha512" + }, + "System.Buffers/4.4.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-AwarXzzoDwX6BgrhjoJsk6tUezZEozOT5Y9QKF94Gl4JK91I4PIIBkBco9068Y9/Dra8Dkbie99kXB8+1BaYKw==", + "path": "system.buffers/4.4.0", + "hashPath": "system.buffers.4.4.0.nupkg.sha512" + }, + "System.Collections/4.0.11": { + "type": "package", + "serviceable": true, + "sha512": "sha512-YUJGz6eFKqS0V//mLt25vFGrrCvOnsXjlvFQs+KimpwNxug9x0Pzy4PlFMU3Q2IzqAa9G2L4LsK3+9vCBK7oTg==", + "path": "system.collections/4.0.11", + "hashPath": "system.collections.4.0.11.nupkg.sha512" + }, + "System.Collections.Concurrent/4.0.12": { + "type": "package", + "serviceable": true, + "sha512": "sha512-2gBcbb3drMLgxlI0fBfxMA31ec6AEyYCHygGse4vxceJan8mRIWeKJ24BFzN7+bi/NFTgdIgufzb94LWO5EERQ==", + "path": "system.collections.concurrent/4.0.12", + "hashPath": "system.collections.concurrent.4.0.12.nupkg.sha512" + }, + "System.Collections.NonGeneric/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-hMxFT2RhhlffyCdKLDXjx8WEC5JfCvNozAZxCablAuFRH74SCV4AgzE8yJCh/73bFnEoZgJ9MJmkjQ0dJmnKqA==", + "path": "system.collections.nongeneric/4.0.1", + "hashPath": "system.collections.nongeneric.4.0.1.nupkg.sha512" + }, + "System.Collections.Specialized/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-/HKQyVP0yH1I0YtK7KJL/28snxHNH/bi+0lgk/+MbURF6ULhAE31MDI+NZDerNWu264YbxklXCCygISgm+HMug==", + "path": "system.collections.specialized/4.0.1", + "hashPath": "system.collections.specialized.4.0.1.nupkg.sha512" + }, + "System.ComponentModel.Annotations/4.4.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-29K3DQ+IGU7LBaMjTo7SI7T7X/tsMtLvz1p56LJ556Iu0Dw3pKZw5g8yCYCWMRxrOF0Hr0FU0FwW0o42y2sb3A==", + "path": "system.componentmodel.annotations/4.4.0", + "hashPath": "system.componentmodel.annotations.4.4.0.nupkg.sha512" + }, + "System.Diagnostics.Debug/4.0.11": { + "type": "package", + "serviceable": true, + "sha512": "sha512-w5U95fVKHY4G8ASs/K5iK3J5LY+/dLFd4vKejsnI/ZhBsWS9hQakfx3Zr7lRWKg4tAw9r4iktyvsTagWkqYCiw==", + "path": "system.diagnostics.debug/4.0.11", + "hashPath": "system.diagnostics.debug.4.0.11.nupkg.sha512" + }, + "System.Diagnostics.Process/4.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-mpVZ5bnlSs3tTeJ6jYyDJEIa6tavhAd88lxq1zbYhkkCu0Pno2+gHXcvZcoygq2d8JxW3gojXqNJMTAshduqZA==", + "path": "system.diagnostics.process/4.1.0", + "hashPath": "system.diagnostics.process.4.1.0.nupkg.sha512" + }, + "System.Diagnostics.TraceSource/4.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-6WVCczFZKXwpWpzd/iJkYnsmWTSFFiU24Xx/YdHXBcu+nFI/ehTgeqdJQFbtRPzbrO3KtRNjvkhtj4t5/WwWsA==", + "path": "system.diagnostics.tracesource/4.0.0", + "hashPath": "system.diagnostics.tracesource.4.0.0.nupkg.sha512" + }, + "System.Diagnostics.Tracing/4.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-vDN1PoMZCkkdNjvZLql592oYJZgS7URcJzJ7bxeBgGtx5UtR5leNm49VmfHGqIffX4FKacHbI3H6UyNSHQknBg==", + "path": "system.diagnostics.tracing/4.1.0", + "hashPath": "system.diagnostics.tracing.4.1.0.nupkg.sha512" + }, + "System.Dynamic.Runtime/4.0.11": { + "type": "package", + "serviceable": true, + "sha512": "sha512-db34f6LHYM0U0JpE+sOmjar27BnqTVkbLJhgfwMpTdgTigG/Hna3m2MYVwnFzGGKnEJk2UXFuoVTr8WUbU91/A==", + "path": "system.dynamic.runtime/4.0.11", + "hashPath": "system.dynamic.runtime.4.0.11.nupkg.sha512" + }, + "System.Globalization/4.0.11": { + "type": "package", + "serviceable": true, + "sha512": "sha512-B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", + "path": "system.globalization/4.0.11", + "hashPath": "system.globalization.4.0.11.nupkg.sha512" + }, + "System.Globalization.Calendars/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-L1c6IqeQ88vuzC1P81JeHmHA8mxq8a18NUBNXnIY/BVb+TCyAaGIFbhpZt60h9FJNmisymoQkHEFSE9Vslja1Q==", + "path": "system.globalization.calendars/4.0.1", + "hashPath": "system.globalization.calendars.4.0.1.nupkg.sha512" + }, + "System.Globalization.Extensions/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-KKo23iKeOaIg61SSXwjANN7QYDr/3op3OWGGzDzz7mypx0Za0fZSeG0l6cco8Ntp8YMYkIQcAqlk8yhm5/Uhcg==", + "path": "system.globalization.extensions/4.0.1", + "hashPath": "system.globalization.extensions.4.0.1.nupkg.sha512" + }, + "System.IO/4.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-3KlTJceQc3gnGIaHZ7UBZO26SHL1SHE4ddrmiwumFnId+CEHP+O8r386tZKaE6zlk5/mF8vifMBzHj9SaXN+mQ==", + "path": "system.io/4.1.0", + "hashPath": "system.io.4.1.0.nupkg.sha512" + }, + "System.IO.FileSystem/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-IBErlVq5jOggAD69bg1t0pJcHaDbJbWNUZTPI96fkYWzwYbN6D9wRHMULLDd9dHsl7C2YsxXL31LMfPI1SWt8w==", + "path": "system.io.filesystem/4.0.1", + "hashPath": "system.io.filesystem.4.0.1.nupkg.sha512" + }, + "System.IO.FileSystem.Primitives/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-kWkKD203JJKxJeE74p8aF8y4Qc9r9WQx4C0cHzHPrY3fv/L/IhWnyCHaFJ3H1QPOH6A93whlQ2vG5nHlBDvzWQ==", + "path": "system.io.filesystem.primitives/4.0.1", + "hashPath": "system.io.filesystem.primitives.4.0.1.nupkg.sha512" + }, + "System.Linq/4.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-bQ0iYFOQI0nuTnt+NQADns6ucV4DUvMdwN6CbkB1yj8i7arTGiTN5eok1kQwdnnNWSDZfIUySQY+J3d5KjWn0g==", + "path": "system.linq/4.1.0", + "hashPath": "system.linq.4.1.0.nupkg.sha512" + }, + "System.Linq.Expressions/4.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-I+y02iqkgmCAyfbqOmSDOgqdZQ5tTj80Akm5BPSS8EeB0VGWdy6X1KCoYe8Pk6pwDoAKZUOdLVxnTJcExiv5zw==", + "path": "system.linq.expressions/4.1.0", + "hashPath": "system.linq.expressions.4.1.0.nupkg.sha512" + }, + "System.Linq.Queryable/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Yn/WfYe9RoRfmSLvUt2JerP0BTGGykCZkQPgojaxgzF2N0oPo+/AhB8TXOpdCcNlrG3VRtsamtK2uzsp3cqRVw==", + "path": "system.linq.queryable/4.0.1", + "hashPath": "system.linq.queryable.4.0.1.nupkg.sha512" + }, + "System.Net.NameResolution/4.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-JdqRdM1Qym3YehqdKIi5LHrpypP4JMfxKQSNCJ2z4WawkG0il+N3XfNeJOxll2XrTnG7WgYYPoeiu/KOwg0DQw==", + "path": "system.net.nameresolution/4.0.0", + "hashPath": "system.net.nameresolution.4.0.0.nupkg.sha512" + }, + "System.Net.Primitives/4.0.11": { + "type": "package", + "serviceable": true, + "sha512": "sha512-hVvfl4405DRjA2408luZekbPhplJK03j2Y2lSfMlny7GHXlkByw1iLnc9mgKW0GdQn73vvMcWrWewAhylXA4Nw==", + "path": "system.net.primitives/4.0.11", + "hashPath": "system.net.primitives.4.0.11.nupkg.sha512" + }, + "System.Net.Security/4.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-uM1JaYJciCc2w7efD6du0EpQ1n5ZQqE6/P43/aI4H5E59qvP+wt3l70KIUF/Ha7NaeXGoGNFPVO0MB80pVHk2g==", + "path": "system.net.security/4.0.0", + "hashPath": "system.net.security.4.0.0.nupkg.sha512" + }, + "System.ObjectModel/4.0.12": { + "type": "package", + "serviceable": true, + "sha512": "sha512-tAgJM1xt3ytyMoW4qn4wIqgJYm7L7TShRZG4+Q4Qsi2PCcj96pXN7nRywS9KkB3p/xDUjc2HSwP9SROyPYDYKQ==", + "path": "system.objectmodel/4.0.12", + "hashPath": "system.objectmodel.4.0.12.nupkg.sha512" + }, + "System.Reflection/4.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", + "path": "system.reflection/4.1.0", + "hashPath": "system.reflection.4.1.0.nupkg.sha512" + }, + "System.Reflection.Emit/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-P2wqAj72fFjpP6wb9nSfDqNBMab+2ovzSDzUZK7MVIm54tBJEPr9jWfSjjoTpPwj1LeKcmX3vr0ttyjSSFM47g==", + "path": "system.reflection.emit/4.0.1", + "hashPath": "system.reflection.emit.4.0.1.nupkg.sha512" + }, + "System.Reflection.Emit.ILGeneration/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Ov6dU8Bu15Bc7zuqttgHF12J5lwSWyTf1S+FJouUXVMSqImLZzYaQ+vRr1rQ0OZ0HqsrwWl4dsKHELckQkVpgA==", + "path": "system.reflection.emit.ilgeneration/4.0.1", + "hashPath": "system.reflection.emit.ilgeneration.4.0.1.nupkg.sha512" + }, + "System.Reflection.Emit.Lightweight/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-sSzHHXueZ5Uh0OLpUQprhr+ZYJrLPA2Cmr4gn0wj9+FftNKXx8RIMKvO9qnjk2ebPYUjZ+F2ulGdPOsvj+MEjA==", + "path": "system.reflection.emit.lightweight/4.0.1", + "hashPath": "system.reflection.emit.lightweight.4.0.1.nupkg.sha512" + }, + "System.Reflection.Extensions/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-GYrtRsZcMuHF3sbmRHfMYpvxZoIN2bQGrYGerUiWLEkqdEUQZhH3TRSaC/oI4wO0II1RKBPlpIa1TOMxIcOOzQ==", + "path": "system.reflection.extensions/4.0.1", + "hashPath": "system.reflection.extensions.4.0.1.nupkg.sha512" + }, + "System.Reflection.Primitives/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", + "path": "system.reflection.primitives/4.0.1", + "hashPath": "system.reflection.primitives.4.0.1.nupkg.sha512" + }, + "System.Reflection.TypeExtensions/4.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-tsQ/ptQ3H5FYfON8lL4MxRk/8kFyE0A+tGPXmVP967cT/gzLHYxIejIYSxp4JmIeFHVP78g/F2FE1mUUTbDtrg==", + "path": "system.reflection.typeextensions/4.1.0", + "hashPath": "system.reflection.typeextensions.4.1.0.nupkg.sha512" + }, + "System.Resources.ResourceManager/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", + "path": "system.resources.resourcemanager/4.0.1", + "hashPath": "system.resources.resourcemanager.4.0.1.nupkg.sha512" + }, + "System.Runtime/4.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-v6c/4Yaa9uWsq+JMhnOFewrYkgdNHNG2eMKuNqRn8P733rNXeRCGvV5FkkjBXn2dbVkPXOsO0xjsEeM1q2zC0g==", + "path": "system.runtime/4.1.0", + "hashPath": "system.runtime.4.1.0.nupkg.sha512" + }, + "System.Runtime.CompilerServices.Unsafe/4.4.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-9dLLuBxr5GNmOfl2jSMcsHuteEg32BEfUotmmUkmZjpR3RpVHE8YQwt0ow3p6prwA1ME8WqDVZqrr8z6H8G+Kw==", + "path": "system.runtime.compilerservices.unsafe/4.4.0", + "hashPath": "system.runtime.compilerservices.unsafe.4.4.0.nupkg.sha512" + }, + "System.Runtime.Extensions/4.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-CUOHjTT/vgP0qGW22U4/hDlOqXmcPq5YicBaXdUR2UiUoLwBT+olO6we4DVbq57jeX5uXH2uerVZhf0qGj+sVQ==", + "path": "system.runtime.extensions/4.1.0", + "hashPath": "system.runtime.extensions.4.1.0.nupkg.sha512" + }, + "System.Runtime.Handles/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-nCJvEKguXEvk2ymk1gqj625vVnlK3/xdGzx0vOKicQkoquaTBJTP13AIYkocSUwHCLNBwUbXTqTWGDxBTWpt7g==", + "path": "system.runtime.handles/4.0.1", + "hashPath": "system.runtime.handles.4.0.1.nupkg.sha512" + }, + "System.Runtime.InteropServices/4.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-16eu3kjHS633yYdkjwShDHZLRNMKVi/s0bY8ODiqJ2RfMhDMAwxZaUaWVnZ2P71kr/or+X9o/xFWtNqz8ivieQ==", + "path": "system.runtime.interopservices/4.1.0", + "hashPath": "system.runtime.interopservices.4.1.0.nupkg.sha512" + }, + "System.Runtime.Numerics/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-+XbKFuzdmLP3d1o9pdHu2nxjNr2OEPqGzKeegPLCUMM71a0t50A/rOcIRmGs9wR7a8KuHX6hYs/7/TymIGLNqg==", + "path": "system.runtime.numerics/4.0.1", + "hashPath": "system.runtime.numerics.4.0.1.nupkg.sha512" + }, + "System.Security.AccessControl/4.4.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-2NRFPX/V81ucKQmqNgGBZrKGH/5ejsvivSGMRum0SMgPnJxwhuNkzVS1+7gC3R2X0f57CtwrPrXPPSe6nOp82g==", + "path": "system.security.accesscontrol/4.4.0", + "hashPath": "system.security.accesscontrol.4.4.0.nupkg.sha512" + }, + "System.Security.Claims/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-4Jlp0OgJLS/Voj1kyFP6MJlIYp3crgfH8kNQk2p7+4JYfc1aAmh9PZyAMMbDhuoolGNtux9HqSOazsioRiDvCw==", + "path": "system.security.claims/4.0.1", + "hashPath": "system.security.claims.4.0.1.nupkg.sha512" + }, + "System.Security.Cryptography.Algorithms/4.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-8JQFxbLVdrtIOKMDN38Fn0GWnqYZw/oMlwOUG/qz1jqChvyZlnUmu+0s7wLx7JYua/nAXoESpHA3iw11QFWhXg==", + "path": "system.security.cryptography.algorithms/4.2.0", + "hashPath": "system.security.cryptography.algorithms.4.2.0.nupkg.sha512" + }, + "System.Security.Cryptography.Cng/4.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-cUJ2h+ZvONDe28Szw3st5dOHdjndhJzQ2WObDEXAWRPEQBtVItVoxbXM/OEsTthl3cNn2dk2k0I3y45igCQcLw==", + "path": "system.security.cryptography.cng/4.2.0", + "hashPath": "system.security.cryptography.cng.4.2.0.nupkg.sha512" + }, + "System.Security.Cryptography.Csp/4.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-/i1Usuo4PgAqgbPNC0NjbO3jPW//BoBlTpcWFD1EHVbidH21y4c1ap5bbEMSGAXjAShhMH4abi/K8fILrnu4BQ==", + "path": "system.security.cryptography.csp/4.0.0", + "hashPath": "system.security.cryptography.csp.4.0.0.nupkg.sha512" + }, + "System.Security.Cryptography.Encoding/4.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-FbKgE5MbxSQMPcSVRgwM6bXN3GtyAh04NkV8E5zKCBE26X0vYW0UtTa2FIgkH33WVqBVxRgxljlVYumWtU+HcQ==", + "path": "system.security.cryptography.encoding/4.0.0", + "hashPath": "system.security.cryptography.encoding.4.0.0.nupkg.sha512" + }, + "System.Security.Cryptography.OpenSsl/4.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-HUG/zNUJwEiLkoURDixzkzZdB5yGA5pQhDP93ArOpDPQMteURIGERRNzzoJlmTreLBWr5lkFSjjMSk8ySEpQMw==", + "path": "system.security.cryptography.openssl/4.0.0", + "hashPath": "system.security.cryptography.openssl.4.0.0.nupkg.sha512" + }, + "System.Security.Cryptography.Primitives/4.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Wkd7QryWYjkQclX0bngpntW5HSlMzeJU24UaLJQ7YTfI8ydAVAaU2J+HXLLABOVJlKTVvAeL0Aj39VeTe7L+oA==", + "path": "system.security.cryptography.primitives/4.0.0", + "hashPath": "system.security.cryptography.primitives.4.0.0.nupkg.sha512" + }, + "System.Security.Cryptography.X509Certificates/4.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-4HEfsQIKAhA1+ApNn729Gi09zh+lYWwyIuViihoMDWp1vQnEkL2ct7mAbhBlLYm+x/L4Rr/pyGge1lIY635e0w==", + "path": "system.security.cryptography.x509certificates/4.1.0", + "hashPath": "system.security.cryptography.x509certificates.4.1.0.nupkg.sha512" + }, + "System.Security.Cryptography.Xml/4.4.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-1Xubvo4i+K+DO6YzVh6vBKmCl5xx/cAoiJEze6VQ+XwVQU25KQC9pPrmniz2EbbJnmoQ5Rm2FFjHsfQAi0Rs+Q==", + "path": "system.security.cryptography.xml/4.4.0", + "hashPath": "system.security.cryptography.xml.4.4.0.nupkg.sha512" + }, + "System.Security.Principal/4.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-On+SKhXY5rzxh/S8wlH1Rm0ogBlu7zyHNxeNBiXauNrhHRXAe9EuX8Yl5IOzLPGU5Z4kLWHMvORDOCG8iu9hww==", + "path": "system.security.principal/4.0.1", + "hashPath": "system.security.principal.4.0.1.nupkg.sha512" + }, + "System.Security.Principal.Windows/4.4.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-pP+AOzt1o3jESOuLmf52YQTF7H3Ng9hTnrOESQiqsnl2IbBh1HInsAMHYtoh75iUYV0OIkHmjvveraYB6zM97w==", + "path": "system.security.principal.windows/4.4.0", + "hashPath": "system.security.principal.windows.4.4.0.nupkg.sha512" + }, + "System.Security.SecureString/4.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-sqzq9GD6/b0yqPuMpgIKBuoLf4VKAj8oAfh4kXSzPaN6eoKY3hRi9C5L27uip25qlU+BGPfb0xh2Rmbwc4jFVA==", + "path": "system.security.securestring/4.0.0", + "hashPath": "system.security.securestring.4.0.0.nupkg.sha512" + }, + "System.Text.Encoding/4.0.11": { + "type": "package", + "serviceable": true, + "sha512": "sha512-U3gGeMlDZXxCEiY4DwVLSacg+DFWCvoiX+JThA/rvw37Sqrku7sEFeVBBBMBnfB6FeZHsyDx85HlKL19x0HtZA==", + "path": "system.text.encoding/4.0.11", + "hashPath": "system.text.encoding.4.0.11.nupkg.sha512" + }, + "System.Text.Encoding.Extensions/4.0.11": { + "type": "package", + "serviceable": true, + "sha512": "sha512-jtbiTDtvfLYgXn8PTfWI+SiBs51rrmO4AAckx4KR6vFK9Wzf6tI8kcRdsYQNwriUeQ1+CtQbM1W4cMbLXnj/OQ==", + "path": "system.text.encoding.extensions/4.0.11", + "hashPath": "system.text.encoding.extensions.4.0.11.nupkg.sha512" + }, + "System.Text.Encodings.Web/4.4.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-l/tYeikqMHX2MD2jzrHDfR9ejrpTTF7wvAEbR51AMvzip1wSJgiURbDik4iv/w7ZgytmTD/hlwpplEhF9bmFNw==", + "path": "system.text.encodings.web/4.4.0", + "hashPath": "system.text.encodings.web.4.4.0.nupkg.sha512" + }, + "System.Threading/4.0.11": { + "type": "package", + "serviceable": true, + "sha512": "sha512-N+3xqIcg3VDKyjwwCGaZ9HawG9aC6cSDI+s7ROma310GQo8vilFZa86hqKppwTHleR/G0sfOzhvgnUxWCR/DrQ==", + "path": "system.threading/4.0.11", + "hashPath": "system.threading.4.0.11.nupkg.sha512" + }, + "System.Threading.Tasks/4.0.11": { + "type": "package", + "serviceable": true, + "sha512": "sha512-k1S4Gc6IGwtHGT8188RSeGaX86Qw/wnrgNLshJvsdNUOPP9etMmo8S07c+UlOAx4K/xLuN9ivA1bD0LVurtIxQ==", + "path": "system.threading.tasks/4.0.11", + "hashPath": "system.threading.tasks.4.0.11.nupkg.sha512" + }, + "System.Threading.Thread/4.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-gIdJqDXlOr5W9zeqFErLw3dsOsiShSCYtF9SEHitACycmvNvY8odf9kiKvp6V7aibc8C4HzzNBkWXjyfn7plbQ==", + "path": "system.threading.thread/4.0.0", + "hashPath": "system.threading.thread.4.0.0.nupkg.sha512" + }, + "System.Threading.ThreadPool/4.0.10": { + "type": "package", + "serviceable": true, + "sha512": "sha512-IMXgB5Vf/5Qw1kpoVgJMOvUO1l32aC+qC3OaIZjWJOjvcxuxNWOK2ZTWWYXfij22NHxT2j1yWX5vlAeQWld9vA==", + "path": "system.threading.threadpool/4.0.10", + "hashPath": "system.threading.threadpool.4.0.10.nupkg.sha512" + } + } +} \ No newline at end of file diff --git a/src/lib/netcoreapp2.0/AspNetCore.Identity.MongoDbCore.dll b/src/lib/netcoreapp2.0/AspNetCore.Identity.MongoDbCore.dll new file mode 100644 index 0000000..f9060e1 Binary files /dev/null and b/src/lib/netcoreapp2.0/AspNetCore.Identity.MongoDbCore.dll differ diff --git a/src/lib/netcoreapp2.0/AspNetCore.Identity.MongoDbCore.xml b/src/lib/netcoreapp2.0/AspNetCore.Identity.MongoDbCore.xml new file mode 100644 index 0000000..8eea8d6 --- /dev/null +++ b/src/lib/netcoreapp2.0/AspNetCore.Identity.MongoDbCore.xml @@ -0,0 +1,1375 @@ + + + + AspNetCore.Identity.MongoDbCore + + + + + The extensions for an object that holds claims. + + + + + Adds a claim to a claim holder, implementing . + + The object holding claims. + The claim you want to add. + Returns true if the claim was added. + + + + Replaces a claim on a claim holder, implementing . + + The object holding claims. + The claim you want to replace. + The new claim you want to set. + Returns true if the claim was replaced. + + + + Checks if an object implementing has a claim. + + The object holding claims. + The claim you want to replace. + Returns true if the claim is present, false otherwise. + + + + The interface for an object that holds claims. + + + + + The version of the role schema + + + + + The claims associated to the role + + + + + Gets or sets the LoginProvider this token is from. + + + + + + Gets or sets the token value. + + + + + Replaces a claim on a claim holder, implementing . + + The object holding claims. + The claim you want to replace. + The new claim you want to set. + Returns true if the claim was replaced. + + + + Represents the password hashing options + + + + + Gets options which use the IdentityV3 compat mode, and set the iteration count to 200000 PBKDF2-SHA256 iterations + (roughly 200ms of work) + + + + + Creates a new instance of a persistence store for roles. + + The type of the class representing a role + + + + Constructs a new instance of . + + The . + The . + + + + Creates a new instance of a persistence store for roles. + + The type of the class representing a role. + The type of the data context class used to access the store. + + + + Constructs a new instance of . + + The . + The . + + + + Creates a new instance of a persistence store for roles. + + The type of the class representing a role. + The type of the data context class used to access the store. + The type of the primary key for a role. + + + + Constructs a new instance of . + + The . + The . + + + + Creates a new instance of a persistence store for roles. + + The type of the class representing a role. + The type of the data context class used to access the store. + The type of the primary key for a role. + The type of the class representing a user role. + The type of the class representing a role claim. + + + + Constructs a new instance of . + + The . + The . + + + + Gets the database context for this store. + + + + + Gets or sets the for any error that occurred with the current operation. + + + + + Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called. + + + True if changes should be automatically persisted, otherwise false. + + + + + Creates a new role in a store as an asynchronous operation. + + The role to create in the store. + The used to propagate notifications that the operation should be canceled. + A that represents the of the asynchronous query. + + + + Updates a role in a store as an asynchronous operation. + + The role to update in the store. + The used to propagate notifications that the operation should be canceled. + A that represents the of the asynchronous query. + + + + Deletes a role from the store as an asynchronous operation. + + The role to delete from the store. + The used to propagate notifications that the operation should be canceled. + A that represents the of the asynchronous query. + + + + Gets the ID for a role from the store as an asynchronous operation. + + The role whose ID should be returned. + The used to propagate notifications that the operation should be canceled. + A that contains the ID of the role. + + + + Gets the name of a role from the store as an asynchronous operation. + + The role whose name should be returned. + The used to propagate notifications that the operation should be canceled. + A that contains the name of the role. + + + + Sets the name of a role in the store as an asynchronous operation. + + The role whose name should be set. + The name of the role. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Converts the provided to a strongly typed key object. + + The id to convert. + An instance of representing the provided . + + + + Converts the provided to its string representation. + + The id to convert. + An representation of the provided . + + + + Finds the role who has the specified ID as an asynchronous operation. + + The role ID to look for. + The used to propagate notifications that the operation should be canceled. + A that result of the look up. + + + + Finds the role who has the specified normalized name as an asynchronous operation. + + The normalized role name to look for. + The used to propagate notifications that the operation should be canceled. + A that result of the look up. + + + + Get a role's normalized name as an asynchronous operation. + + The role whose normalized name should be retrieved. + The used to propagate notifications that the operation should be canceled. + A that contains the name of the role. + + + + Set a role's normalized name as an asynchronous operation. + + The role whose normalized name should be set. + The normalized name to set + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Throws if this class has been disposed. + + + + + Dispose the stores + + + + + Get the claims associated with the specified as an asynchronous operation. + + The role whose claims should be retrieved. + The used to propagate notifications that the operation should be canceled. + A that contains the claims granted to a role. + + + + Adds the given to the specified . + + The role to add the claim to. + The claim to add to the role. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Removes the given from the specified . + + The role to remove the claim from. + The claim to remove from the role. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + A navigation property for the roles the store contains. + + + + + Creates a entity representing a role claim. + + The associated role. + The associated claim. + The role claim entity. + + + + Creates a new instance of a persistence store for the specified user type. + + The type representing a user. + + + + Constructs a new instance of . + + The . + The . + + + + Represents a new instance of a persistence store for the specified user and role types. + + The type representing a user. + The type of the data context class used to access the store. + + + + Constructs a new instance of . + + The . + The . + + + + Represents a new instance of a persistence store for the specified user and role types. + + The type representing a user. + The type of the data context class used to access the store. + The type of the primary key for a role. + + + + Constructs a new instance of . + + The . + The . + + + + Represents a new instance of a persistence store for the specified user and role types. + + The type representing a user. + The type of the data context class used to access the store. + The type of the primary key for a role. + The type representing a claim. + The type representing a user role. + The type representing a user external login. + The type representing a user token. + The type representing a role claim. + + + + Creates a new instance of the store. + + The context used to access the store. + The used to describe store errors. + + + + Gets the database context for this store. + + + + + Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called. + + + True if changes should be automatically persisted, otherwise false. + + + + Saves the current store. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Creates the specified in the user store. + + The user to create. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation, containing the of the creation operation. + + + + Updates the specified in the user store. + + The user to update. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation, containing the of the update operation. + + + + Deletes the specified from the user store. + + The user to delete. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation, containing the of the update operation. + + + + Finds and returns a user, if any, who has the specified . + + The user ID to search for. + The used to propagate notifications that the operation should be canceled. + + The that represents the asynchronous operation, containing the user matching the specified if it exists. + + + + + Finds and returns a user, if any, who has the specified normalized user name. + + The normalized user name to search for. + The used to propagate notifications that the operation should be canceled. + + The that represents the asynchronous operation, containing the user matching the specified if it exists. + + + + + A navigation property for the users the store contains. + + + + + Return a user with the matching userId if it exists. + + The user's id. + The used to propagate notifications that the operation should be canceled. + The user if it exists. + + + + Return a user login with the matching userId, provider, providerKey if it exists. + + The user's id. + The login provider name. + The key provided by the to identify a user. + The used to propagate notifications that the operation should be canceled. + The user login if it exists. + + + + Return a user login with provider, providerKey if it exists. + + The login provider name. + The key provided by the to identify a user. + The used to propagate notifications that the operation should be canceled. + The user login if it exists. + + + + Get the claims associated with the specified as an asynchronous operation. + + The user whose claims should be retrieved. + The used to propagate notifications that the operation should be canceled. + A that contains the claims granted to a user. + + + + Adds the given to the specified . + + The user to add the claim to. + The claim to add to the user. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Replaces the on the specified , with the . + + The user to replace the claim on. + The claim replace. + The new claim replacing the . + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Removes the given from the specified . + + The user to remove the claims from. + The claim to remove. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Adds the given to the specified . + + The user to add the login to. + The login to add to the user. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Removes the given from the specified . + + The user to remove the login from. + The login to remove from the user. + The key provided by the to identify a user. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Retrieves the associated logins for the specified . + + The user whose associated logins to retrieve. + The used to propagate notifications that the operation should be canceled. + + The for the asynchronous operation, containing a list of for the specified , if any. + + + + + Retrieves the user associated with the specified login provider and login provider key. + + The login provider who provided the . + The key provided by the to identify a user. + The used to propagate notifications that the operation should be canceled. + + The for the asynchronous operation, containing the user, if any which matched the specified login provider and key. + + + + + Gets the user, if any, associated with the specified, normalized email address. + + The normalized email address to return the user for. + The used to propagate notifications that the operation should be canceled. + + The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address. + + + + + Retrieves all users with the specified claim. + + The claim whose users should be retrieved. + The used to propagate notifications that the operation should be canceled. + + The contains a list of users, if any, that contain the specified claim. + + + + + Find a user token if it exists. + + The token owner. + The login provider for the token. + The name of the token. + The used to propagate notifications that the operation should be canceled. + The user token if it exists. + + + + Add a new user token. + + The token to be added. + + + + + Remove a new user token. + + The token to be removed. + + + + + Sets the given for the specified . + + The user whose name should be set. + The user name to set. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets the given normalized name for the specified . + + The user whose name should be set. + The normalized name to set. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets the password hash for a user. + + The user to set the password hash for. + The password hash to set. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets the flag indicating whether the specified 's email address has been confirmed or not. + + The user whose email confirmation status should be set. + A flag indicating if the email address has been confirmed, true if the address is confirmed otherwise false. + The used to propagate notifications that the operation should be canceled. + The task object representing the asynchronous operation. + + + + Sets the address for a . + + The user whose email should be set. + The email to set. + The used to propagate notifications that the operation should be canceled. + The task object representing the asynchronous operation. + + + + Sets the normalized email for the specified . + + The user whose email address to set. + The normalized email to set for the specified . + The used to propagate notifications that the operation should be canceled. + The task object representing the asynchronous operation. + + + + Locks out a user until the specified end date has passed. Setting a end date in the past immediately unlocks a user. + + The user whose lockout date should be set. + The after which the 's lockout should end. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Records that a failed access has occurred, incrementing the failed access count. + + The user whose cancellation count should be incremented. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation, containing the incremented failed access count. + + + + Resets a user's failed access count. + + The user whose failed access count should be reset. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + This is typically called after the account is successfully accessed. + + + + Set the flag indicating if the specified can be locked out.. + + The user whose ability to be locked out should be set. + A flag indicating if lock out can be enabled for the specified . + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets the telephone number for the specified . + + The user whose telephone number should be set. + The telephone number to set. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets a flag indicating if the specified 's phone number has been confirmed.. + + The user whose telephone number confirmation status should be set. + A flag indicating whether the user's telephone number has been confirmed. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets the provided security for the specified . + + The user whose security stamp should be set. + The security stamp to set. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets a flag indicating whether the specified has two factor authentication enabled or not, + as an asynchronous operation. + + The user whose two factor authentication enabled status should be set. + A flag indicating whether the specified has two factor authentication enabled. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets the token value for a particular user. + + The user. + The authentication provider for the token. + The name of the token. + The value of the token. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Deletes a token for a user. + + The user. + The authentication provider for the token. + The name of the token. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Returns the token value. + + The user. + The authentication provider for the token. + The name of the token. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets the authenticator key for the specified . + + The user whose authenticator key should be set. + The authenticator key to set. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Get the authenticator key for the specified . + + The user whose security stamp should be set. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation, containing the security stamp for the specified . + + + + Returns how many recovery code are still valid for a user. + + The user who owns the recovery code. + The used to propagate notifications that the operation should be canceled. + The number of valid recovery codes for the user.. + + + + Updates the recovery codes for the user while invalidating any previous recovery codes. + + The user to store new recovery codes for. + The new recovery codes for the user. + The used to propagate notifications that the operation should be canceled. + The new recovery codes for the user. + + + + Returns whether a recovery code is valid for a user. Note: recovery codes are only valid + once, and will be invalid after use. + + The user who owns the recovery code. + The recovery code to use. + The used to propagate notifications that the operation should be canceled. + True if the recovery code was found for the user. + + + + Represents a new instance of a persistence store for users, using the default implementation + of with a string as a primary key. + + + + + Constructs a new instance of . + + The . + The . + + + + Creates a new instance of a persistence store for the specified user type. + + The type representing a user. + + + + Constructs a new instance of . + + The . + The . + + + + Represents a new instance of a persistence store for the specified user and role types. + + The type representing a user. + The type representing a role. + The type of the data context class used to access the store. + + + + Constructs a new instance of . + + The . + The . + + + + Represents a new instance of a persistence store for the specified user and role types. + + The type representing a user. + The type representing a role. + The type of the data context class used to access the store. + The type of the primary key for a role. + + + + Constructs a new instance of . + + The . + The . + + + + Represents a new instance of a persistence store for the specified user and role types. + + The type representing a user. + The type representing a role. + The type of the data context class used to access the store. + The type of the primary key for a role. + The type representing a claim. + The type representing a user role. + The type representing a user external login. + The type representing a user token. + The type representing a role claim. + + + + Creates a new instance of the store. + + The context used to access the store. + The used to describe store errors. + + + + Gets the database context for this store. + + + + + Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called. + + + True if changes should be automatically persisted, otherwise false. + + + + Saves the current store. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Creates the specified in the user store. + + The user to create. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation, containing the of the creation operation. + + + + Updates the specified in the user store. + + The user to update. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation, containing the of the update operation. + + + + Deletes the specified from the user store. + + The user to delete. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation, containing the of the update operation. + + + + Finds and returns a user, if any, who has the specified . + + The user ID to search for. + The used to propagate notifications that the operation should be canceled. + + The that represents the asynchronous operation, containing the user matching the specified if it exists. + + + + + Finds and returns a user, if any, who has the specified normalized user name. + + The normalized user name to search for. + The used to propagate notifications that the operation should be canceled. + + The that represents the asynchronous operation, containing the user matching the specified if it exists. + + + + + A navigation property for the users the store contains. + + + + + Return a role with the normalized name if it exists. + + The normalized role name. + The used to propagate notifications that the operation should be canceled. + The role if it exists. + + + + Return a user role for the userId and roleId if it exists. + + The user's id. + The role's id. + The used to propagate notifications that the operation should be canceled. + The user role if it exists. + + + + Return a user with the matching userId if it exists. + + The user's id. + The used to propagate notifications that the operation should be canceled. + The user if it exists. + + + + Return a user login with the matching userId, provider, providerKey if it exists. + + The user's id. + The login provider name. + The key provided by the to identify a user. + The used to propagate notifications that the operation should be canceled. + The user login if it exists. + + + + Return a user login with provider, providerKey if it exists. + + The login provider name. + The key provided by the to identify a user. + The used to propagate notifications that the operation should be canceled. + The user login if it exists. + + + + Adds the given to the specified . + + The user to add the role to. + The role to add. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Removes the given from the specified . + + The user to remove the role from. + The role to remove. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Retrieves the roles the specified is a member of. + + The user whose roles should be retrieved. + The used to propagate notifications that the operation should be canceled. + A that contains the roles the user is a member of. + + + + Returns a flag indicating if the specified user is a member of the give . + + The user whose role membership should be checked. + The role to check membership of + The used to propagate notifications that the operation should be canceled. + A containing a flag indicating if the specified user is a member of the given group. If the + user is a member of the group the returned value with be true, otherwise it will be false. + + + + Get the claims associated with the specified as an asynchronous operation. + + The user whose claims should be retrieved. + The used to propagate notifications that the operation should be canceled. + A that contains the claims granted to a user. + + + + Adds the given to the specified . + + The user to add the claim to. + The claim to add to the user. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Replaces the on the specified , with the . + + The user to replace the claim on. + The claim replace. + The new claim replacing the . + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Removes the given from the specified . + + The user to remove the claims from. + The claim to remove. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Adds the given to the specified . + + The user to add the login to. + The login to add to the user. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Removes the given from the specified . + + The user to remove the login from. + The login to remove from the user. + The key provided by the to identify a user. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Retrieves the associated logins for the specified . + + The user whose associated logins to retrieve. + The used to propagate notifications that the operation should be canceled. + + The for the asynchronous operation, containing a list of for the specified , if any. + + + + + Retrieves the user associated with the specified login provider and login provider key. + + The login provider who provided the . + The key provided by the to identify a user. + The used to propagate notifications that the operation should be canceled. + + The for the asynchronous operation, containing the user, if any which matched the specified login provider and key. + + + + + Gets the user, if any, associated with the specified, normalized email address. + + The normalized email address to return the user for. + The used to propagate notifications that the operation should be canceled. + + The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address. + + + + + Retrieves all users with the specified claim. + + The claim whose users should be retrieved. + The used to propagate notifications that the operation should be canceled. + + The contains a list of users, if any, that contain the specified claim. + + + + + Retrieves all users in the specified role. + + The role whose users should be retrieved. + The used to propagate notifications that the operation should be canceled. + + The contains a list of users, if any, that are in the specified role. + + + + + Find a user token if it exists. + + The token owner. + The login provider for the token. + The name of the token. + The used to propagate notifications that the operation should be canceled. + The user token if it exists. + + + + Add a new user token. + + The token to be added. + + + + + Remove a new user token. + + The token to be removed. + + + + + Sets the given for the specified . + + The user whose name should be set. + The user name to set. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets the given normalized name for the specified . + + The user whose name should be set. + The normalized name to set. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets the password hash for a user. + + The user to set the password hash for. + The password hash to set. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets the flag indicating whether the specified 's email address has been confirmed or not. + + The user whose email confirmation status should be set. + A flag indicating if the email address has been confirmed, true if the address is confirmed otherwise false. + The used to propagate notifications that the operation should be canceled. + The task object representing the asynchronous operation. + + + + Sets the address for a . + + The user whose email should be set. + The email to set. + The used to propagate notifications that the operation should be canceled. + The task object representing the asynchronous operation. + + + + Sets the normalized email for the specified . + + The user whose email address to set. + The normalized email to set for the specified . + The used to propagate notifications that the operation should be canceled. + The task object representing the asynchronous operation. + + + + Locks out a user until the specified end date has passed. Setting a end date in the past immediately unlocks a user. + + The user whose lockout date should be set. + The after which the 's lockout should end. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Records that a failed access has occurred, incrementing the failed access count. + + The user whose cancellation count should be incremented. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation, containing the incremented failed access count. + + + + Resets a user's failed access count. + + The user whose failed access count should be reset. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + This is typically called after the account is successfully accessed. + + + + Set the flag indicating if the specified can be locked out.. + + The user whose ability to be locked out should be set. + A flag indicating if lock out can be enabled for the specified . + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets the telephone number for the specified . + + The user whose telephone number should be set. + The telephone number to set. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets a flag indicating if the specified 's phone number has been confirmed.. + + The user whose telephone number confirmation status should be set. + A flag indicating whether the user's telephone number has been confirmed. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets the provided security for the specified . + + The user whose security stamp should be set. + The security stamp to set. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets a flag indicating whether the specified has two factor authentication enabled or not, + as an asynchronous operation. + + The user whose two factor authentication enabled status should be set. + A flag indicating whether the specified has two factor authentication enabled. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets the token value for a particular user. + + The user. + The authentication provider for the token. + The name of the token. + The value of the token. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Deletes a token for a user. + + The user. + The authentication provider for the token. + The name of the token. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Returns the token value. + + The user. + The authentication provider for the token. + The name of the token. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Sets the authenticator key for the specified . + + The user whose authenticator key should be set. + The authenticator key to set. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation. + + + + Get the authenticator key for the specified . + + The user whose security stamp should be set. + The used to propagate notifications that the operation should be canceled. + The that represents the asynchronous operation, containing the security stamp for the specified . + + + + Returns how many recovery code are still valid for a user. + + The user who owns the recovery code. + The used to propagate notifications that the operation should be canceled. + The number of valid recovery codes for the user.. + + + + Updates the recovery codes for the user while invalidating any previous recovery codes. + + The user to store new recovery codes for. + The new recovery codes for the user. + The used to propagate notifications that the operation should be canceled. + The new recovery codes for the user. + + + + Returns whether a recovery code is valid for a user. Note: recovery codes are only valid + once, and will be invalid after use. + + The user who owns the recovery code. + The recovery code to use. + The used to propagate notifications that the operation should be canceled. + True if the recovery code was found for the user. + + + + Une classe de ressource fortement typée destinée, entre autres, à la consultation des chaînes localisées. + + + + + Retourne l'instance ResourceManager mise en cache utilisée par cette classe. + + + + + Remplace la propriété CurrentUICulture du thread actuel pour toutes + les recherches de ressources à l'aide de cette classe de ressource fortement typée. + + + + + Recherche une chaîne localisée semblable à AddEntityFrameworkStores can only be called with a role that derives from IdentityRole<TKey, TUserRole, TRoleClaim>.. + + + + + Recherche une chaîne localisée semblable à AddEntityFrameworkStores can only be called with a user that derives from IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken>.. + + + + + Recherche une chaîne localisée semblable à Role {0} does not exist.. + + + + + Recherche une chaîne localisée semblable à Value cannot be null or empty.. + + + + diff --git a/src/lib/netcoreapp2.0/Resources.resx b/src/lib/netcoreapp2.0/Resources.resx new file mode 100644 index 0000000..eb19639 --- /dev/null +++ b/src/lib/netcoreapp2.0/Resources.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + AddEntityFrameworkStores can only be called with a role that derives from IdentityRole<TKey, TUserRole, TRoleClaim>. + error when the role does not derive from IdentityRole + + + AddEntityFrameworkStores can only be called with a user that derives from IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken>. + error when the user does not derive from IdentityUser + + + Role {0} does not exist. + error when a role does not exist + + + Value cannot be null or empty. + error when something cannot be null or empty + + \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.IntegrationTests.csproj b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.IntegrationTests.csproj new file mode 100644 index 0000000..349ac06 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.IntegrationTests.csproj @@ -0,0 +1,30 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/ApiConsistencyTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/ApiConsistencyTest.cs new file mode 100644 index 0000000..58cb982 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/ApiConsistencyTest.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using Microsoft.AspNetCore.Identity.Test; +using Microsoft.AspNetCore.Identity; + +namespace AspNetCore.Identity.MongoDbCore.Test +{ + public class ApiConsistencyTest : ApiConsistencyTestBase + { + protected override Assembly TargetAssembly => typeof(IdentityUser).GetTypeInfo().Assembly; + } +} diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/CustomPocoTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/CustomPocoTest.cs new file mode 100644 index 0000000..21efa8a --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/CustomPocoTest.cs @@ -0,0 +1,150 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace AspNetCore.Identity.MongoDbCore.Test +{ + //public class CustomPocoTest : IClassFixture + //{ + // private readonly MongoDatabaseFixture _fixture; + + // public CustomPocoTest(MongoDatabaseFixture fixture) + // { + // _fixture = fixture; + // } + + // public class User where TKey : IEquatable + // { + // public TKey Id { get; set; } + // public string UserName { get; set; } + // } + + // public class CustomDbContext : DbContext where TKey : IEquatable + // { + // public CustomDbContext(DbContextOptions options) : base(options) + // { } + + // public DbSet> Users { get; set; } + + // } + + // public CustomDbContext GetContext() where TKey : IEquatable + // { + // return DbUtil.Create>(_fixture.ConnectionString); + // } + + // public CustomDbContext CreateContext(bool delete = false) where TKey : IEquatable + // { + // var db = GetContext(); + // if (delete) + // { + // db.Database.EnsureDeleted(); + // } + // db.Database.EnsureCreated(); + // return db; + // } + + // [Fact] + // public async Task CanUpdateNameGuid() + // { + // using (var db = CreateContext(true)) + // { + // var oldName = Guid.NewGuid().ToString(); + // var user = new User { UserName = oldName, Id = Guid.NewGuid() }; + // db.Users.Add(user); + // await db.SaveChangesAsync(); + // var newName = Guid.NewGuid().ToString(); + // user.UserName = newName; + // await db.SaveChangesAsync(); + // Assert.Null(db.Users.SingleOrDefault(u => u.UserName == oldName)); + // Assert.Equal(user, db.Users.Single(u => u.UserName == newName)); + // } + // } + + // [Fact] + // public async Task CanUpdateNameString() + // { + // using (var db = CreateContext(true)) + // { + // var oldName = Guid.NewGuid().ToString(); + // var user = new User { UserName = oldName, Id = Guid.NewGuid().ToString() }; + // db.Users.Add(user); + // await db.SaveChangesAsync(); + // var newName = Guid.NewGuid().ToString(); + // user.UserName = newName; + // await db.SaveChangesAsync(); + // Assert.Null(db.Users.SingleOrDefault(u => u.UserName == oldName)); + // Assert.Equal(user, db.Users.Single(u => u.UserName == newName)); + // } + // } + + // [Fact] + // public async Task CanCreateUserInt() + // { + // using (var db = CreateContext(true)) + // { + // var user = new User(); + // db.Users.Add(user); + // await db.SaveChangesAsync(); + // user.UserName = "Boo"; + // await db.SaveChangesAsync(); + // var fetch = db.Users.First(u => u.UserName == "Boo"); + // Assert.Equal(user, fetch); + // } + // } + + // [Fact] + // public async Task CanCreateUserIntViaSet() + // { + // using (var db = CreateContext(true)) + // { + // var user = new User(); + // var users = db.Set>(); + // users.Add(user); + // await db.SaveChangesAsync(); + // user.UserName = "Boo"; + // await db.SaveChangesAsync(); + // var fetch = users.First(u => u.UserName == "Boo"); + // Assert.Equal(user, fetch); + // } + // } + + // [Fact] + // public async Task CanUpdateNameInt() + // { + // using (var db = CreateContext(true)) + // { + // var oldName = Guid.NewGuid().ToString(); + // var user = new User { UserName = oldName }; + // db.Users.Add(user); + // await db.SaveChangesAsync(); + // var newName = Guid.NewGuid().ToString(); + // user.UserName = newName; + // await db.SaveChangesAsync(); + // Assert.Null(db.Users.SingleOrDefault(u => u.UserName == oldName)); + // Assert.Equal(user, db.Users.Single(u => u.UserName == newName)); + // } + // } + + // [Fact] + // public async Task CanUpdateNameIntWithSet() + // { + // using (var db = CreateContext(true)) + // { + // var oldName = Guid.NewGuid().ToString(); + // var user = new User { UserName = oldName }; + // db.Set>().Add(user); + // await db.SaveChangesAsync(); + // var newName = Guid.NewGuid().ToString(); + // user.UserName = newName; + // await db.SaveChangesAsync(); + // Assert.Null(db.Set>().SingleOrDefault(u => u.UserName == oldName)); + // Assert.Equal(user, db.Set>().Single(u => u.UserName == newName)); + // } + // } + //} +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/SqlStoreOnlyUsersTestBase.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/SqlStoreOnlyUsersTestBase.cs new file mode 100644 index 0000000..cffdb8e --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/SqlStoreOnlyUsersTestBase.cs @@ -0,0 +1,190 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.Test; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using AspNetCore.Identity.MongoDbCore.IntegrationTests.Infrastructure; +using MongoDbGenericRepository; +using AspNetCore.Identity.MongoDbCore.Models; +using AspNetCore.Identity.MongoDbCore; +using MongoDB.Driver; + +namespace AspNetCore.Identity.MongoDbCore.Test +{ + public abstract class SqlStoreOnlyUsersTestBase : UserManagerSpecificationTestBase, IClassFixture> + where TUser : MongoIdentityUser, new() + where TKey : IEquatable + { + private readonly MongoDatabaseFixture _fixture; + + protected SqlStoreOnlyUsersTestBase(MongoDatabaseFixture fixture) + { + _fixture = fixture; + } + + protected override bool ShouldSkipDbTests() + { + return false; + } + + protected override TUser CreateTestUser(string namePrefix = "", string email = "", string phoneNumber = "", + bool lockoutEnabled = false, DateTimeOffset? lockoutEnd = default(DateTimeOffset?), bool useNamePrefixAsUserName = false) + { + var user = new TUser + { + UserName = useNamePrefixAsUserName ? namePrefix : string.Format("{0}{1}", namePrefix, Guid.NewGuid()), + Email = email, + PhoneNumber = phoneNumber, + LockoutEnabled = lockoutEnabled, + LockoutEnd = lockoutEnd + }; + _fixture.UsersToDelete.Add(user); + return user; + } + + protected override Expression> UserNameEqualsPredicate(string userName) => u => u.UserName == userName; + + protected override Expression> UserNameStartsWithPredicate(string userName) => u => u.UserName.StartsWith(userName); + + public IMongoDbContext CreateContext() + { + return Container.MongoRepository.Context; + } + + + protected override void AddUserStore(IServiceCollection services, object context = null) + { + services.AddSingleton>(new MongoUserOnlyStore(Container.MongoRepository.Context)); + } + + protected override void SetUserPasswordHash(TUser user, string hashedPassword) + { + user.PasswordHash = hashedPassword; + } + + [Fact] + public async Task DeleteUserRemovesTokensTest() + { + // Need fail if not empty? + var userMgr = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await userMgr.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await userMgr.SetAuthenticationTokenAsync(user, "provider", "test", "value")); + + Assert.Equal("value", await userMgr.GetAuthenticationTokenAsync(user, "provider", "test")); + + IdentityResultAssert.IsSuccess(await userMgr.DeleteAsync(user)); + + Assert.Null(await userMgr.GetAuthenticationTokenAsync(user, "provider", "test")); + } + + private IQueryable GetQueryable() + { + return Container.MongoRepository.Context.GetCollection().AsQueryable(); + } + + [Fact] + public void CanCreateUserUsingEF() + { + var user = CreateTestUser(); + Container.MongoRepository.AddOne(user); + Assert.True(GetQueryable().Any(u => u.UserName == user.UserName)); + Assert.NotNull(GetQueryable().FirstOrDefault(u => u.UserName == user.UserName)); + } + + [Fact] + public async Task CanCreateUsingManager() + { + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.DeleteAsync(user)); + } + + private async Task LazyLoadTestSetup(IMongoDbContext db, TUser user) + { + var context = CreateContext(); + var manager = CreateManager(context); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.AddLoginAsync(user, new UserLoginInfo("provider", user.Id.ToString(), "display"))); + Claim[] userClaims = + { + new Claim("Whatever", "Value"), + new Claim("Whatever2", "Value2") + }; + foreach (var c in userClaims) + { + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, c)); + } + } + + [Fact] + public async Task LoadFromDbFindByIdTest() + { + var db = CreateContext(); + var user = CreateTestUser(); + await LazyLoadTestSetup(db, user); + + db = CreateContext(); + var manager = CreateManager(db); + + var userById = await manager.FindByIdAsync(user.Id.ToString()); + Assert.Equal(2, (await manager.GetClaimsAsync(userById)).Count); + Assert.Equal(1, (await manager.GetLoginsAsync(userById)).Count); + Assert.Equal(2, (await manager.GetRolesAsync(userById)).Count); + } + + [Fact] + public async Task LoadFromDbFindByNameTest() + { + var db = CreateContext(); + var user = CreateTestUser(); + await LazyLoadTestSetup(db, user); + + db = CreateContext(); + var manager = CreateManager(db); + var userByName = await manager.FindByNameAsync(user.UserName); + Assert.Equal(2, (await manager.GetClaimsAsync(userByName)).Count); + Assert.Equal(1, (await manager.GetLoginsAsync(userByName)).Count); + Assert.Equal(2, (await manager.GetRolesAsync(userByName)).Count); + } + + [Fact] + public async Task LoadFromDbFindByLoginTest() + { + var db = CreateContext(); + var user = CreateTestUser(); + await LazyLoadTestSetup(db, user); + + db = CreateContext(); + var manager = CreateManager(db); + var userByLogin = await manager.FindByLoginAsync("provider", user.Id.ToString()); + Assert.Equal(2, (await manager.GetClaimsAsync(userByLogin)).Count); + Assert.Equal(1, (await manager.GetLoginsAsync(userByLogin)).Count); + Assert.Equal(2, (await manager.GetRolesAsync(userByLogin)).Count); + } + + [Fact] + public async Task LoadFromDbFindByEmailTest() + { + var db = CreateContext(); + var user = CreateTestUser(); + user.Email = "fooz@fizzy.pop"; + await LazyLoadTestSetup(db, user); + + db = CreateContext(); + var manager = CreateManager(db); + var userByEmail = await manager.FindByEmailAsync(user.Email); + Assert.Equal(2, (await manager.GetClaimsAsync(userByEmail)).Count); + Assert.Equal(1, (await manager.GetLoginsAsync(userByEmail)).Count); + Assert.Equal(2, (await manager.GetRolesAsync(userByEmail)).Count); + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/SqlStoreTestBase.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/SqlStoreTestBase.cs new file mode 100644 index 0000000..ec145f2 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/SqlStoreTestBase.cs @@ -0,0 +1,264 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Data.SqlClient; +using System.Linq; +using System.Linq.Expressions; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity.Test; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; +using AspNetCore.Identity.MongoDbCore.Models; +using AspNetCore.Identity.MongoDbCore.Extensions; +using AspNetCore.Identity.MongoDbCore; +using MongoDB.Driver; +using AspNetCore.Identity.MongoDbCore.IntegrationTests.Infrastructure; +using MongoDbGenericRepository; +using Microsoft.AspNetCore.Identity; + +namespace AspNetCore.Identity.MongoDbCore.Test +{ + // TODO: Add test variation with non IdentityDbContext + + public abstract class SqlStoreTestBase : IdentitySpecificationTestBase, + IClassFixture> + where TUser : MongoIdentityUser, new() + where TRole : MongoIdentityRole, new() + where TKey : IEquatable + { + private readonly MongoDatabaseFixture _fixture; + + protected SqlStoreTestBase(MongoDatabaseFixture fixture) + { + _fixture = fixture; + } + + protected override void SetupIdentityServices(IServiceCollection services) + { + services.AddSingleton(); + // configure the default type name + services.ConfigureMongoDbIdentity(Container.MongoDbIdentityConfiguration, Container.MongoRepository.Context); + + services.AddLogging(); + services.AddSingleton>>(new TestLogger>()); + services.AddSingleton>>(new TestLogger>()); + } + + protected override bool ShouldSkipDbTests() + { + return false; + } + + protected override TUser CreateTestUser(string namePrefix = "", string email = "", string phoneNumber = "", + bool lockoutEnabled = false, DateTimeOffset? lockoutEnd = default(DateTimeOffset?), bool useNamePrefixAsUserName = false) + { + var user = new TUser + { + UserName = useNamePrefixAsUserName ? namePrefix : string.Format("{0}{1}", namePrefix, Guid.NewGuid()), + Email = email, + PhoneNumber = phoneNumber, + LockoutEnabled = lockoutEnabled, + LockoutEnd = lockoutEnd + }; + _fixture.UsersToDelete.Add(user); + return user; + } + + protected override TRole CreateTestRole(string roleNamePrefix = "", bool useRoleNamePrefixAsRoleName = false) + { + var roleName = useRoleNamePrefixAsRoleName ? roleNamePrefix : string.Format("{0}{1}", roleNamePrefix, Guid.NewGuid()); + var role = new TRole() { Name = roleName }; + _fixture.RolesToDelete.Add(role); + return role; + } + + protected override Expression> RoleNameEqualsPredicate(string roleName) => r => r.Name == roleName; + + protected override Expression> UserNameEqualsPredicate(string userName) => u => u.UserName == userName; + + protected override Expression> RoleNameStartsWithPredicate(string roleName) => r => r.Name.StartsWith(roleName); + + protected override Expression> UserNameStartsWithPredicate(string userName) => u => u.UserName.StartsWith(userName); + + + protected override void AddUserStore(IServiceCollection services, object context = null) + { + services.AddSingleton>(new MongoUserStore(Container.MongoRepository.Context)); + } + + protected override void AddRoleStore(IServiceCollection services, object context = null) + { + services.AddSingleton>(new MongoRoleStore(Container.MongoRepository.Context)); + } + + protected override void SetUserPasswordHash(TUser user, string hashedPassword) + { + user.PasswordHash = hashedPassword; + } + + [Fact] + public async Task DeleteRoleNonEmptySucceedsTest() + { + var userMgr = CreateManager(); + var roleMgr = CreateRoleManager(); + var roleName = "delete" + Guid.NewGuid().ToString(); + var role = CreateTestRole(roleName, useRoleNamePrefixAsRoleName: true); + Assert.False(await roleMgr.RoleExistsAsync(roleName)); + IdentityResultAssert.IsSuccess(await roleMgr.CreateAsync(role)); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await userMgr.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await userMgr.AddToRoleAsync(user, roleName)); + var roles = await userMgr.GetRolesAsync(user); + Assert.Single(roles); + IdentityResultAssert.IsSuccess(await roleMgr.DeleteAsync(role)); + Assert.Null(await roleMgr.FindByNameAsync(roleName)); + Assert.False(await roleMgr.RoleExistsAsync(roleName)); + // REVIEW: We should throw if deleteing a non empty role? + roles = await userMgr.GetRolesAsync(user); + + Assert.Empty(roles); + } + + [Fact] + public async Task DeleteUserRemovesFromRoleTest() + { + // Need fail if not empty? + var userMgr = CreateManager(); + var roleMgr = CreateRoleManager(); + var roleName = "deleteUserRemove" + Guid.NewGuid().ToString(); + var role = CreateTestRole(roleName, useRoleNamePrefixAsRoleName: true); + Assert.False(await roleMgr.RoleExistsAsync(roleName)); + IdentityResultAssert.IsSuccess(await roleMgr.CreateAsync(role)); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await userMgr.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await userMgr.AddToRoleAsync(user, roleName)); + + var roles = await userMgr.GetRolesAsync(user); + Assert.Single(roles); + + IdentityResultAssert.IsSuccess(await userMgr.DeleteAsync(user)); + + roles = await userMgr.GetRolesAsync(user); + Assert.Empty(roles); + } + + [Fact] + public async Task DeleteUserRemovesTokensTest() + { + // Need fail if not empty? + var userMgr = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await userMgr.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await userMgr.SetAuthenticationTokenAsync(user, "provider", "test", "value")); + + Assert.Equal("value", await userMgr.GetAuthenticationTokenAsync(user, "provider", "test")); + + IdentityResultAssert.IsSuccess(await userMgr.DeleteAsync(user)); + + Assert.Null(await userMgr.GetAuthenticationTokenAsync(user, "provider", "test")); + } + + private IQueryable GetQueryable() + { + return Container.MongoRepository.Context.GetCollection().AsQueryable(); + } + + [Fact] + public void CanCreateUserUsingEF() + { + var user = CreateTestUser(); + Container.MongoRepository.AddOne(user); + Assert.True(GetQueryable().Any(u => u.UserName == user.UserName)); + Assert.NotNull(GetQueryable().FirstOrDefault(u => u.UserName == user.UserName)); + } + + [Fact] + public async Task CanCreateUsingManager() + { + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.DeleteAsync(user)); + } + + private async Task LazyLoadTestSetup(TUser user) + { + var manager = CreateManager(); + var role = CreateRoleManager(); + var admin = CreateTestRole("Admin" + Guid.NewGuid().ToString()); + var local = CreateTestRole("Local" + Guid.NewGuid().ToString()); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.AddLoginAsync(user, new UserLoginInfo("provider", user.Id.ToString(), "display"))); + IdentityResultAssert.IsSuccess(await role.CreateAsync(admin)); + IdentityResultAssert.IsSuccess(await role.CreateAsync(local)); + IdentityResultAssert.IsSuccess(await manager.AddToRoleAsync(user, admin.Name)); + IdentityResultAssert.IsSuccess(await manager.AddToRoleAsync(user, local.Name)); + Claim[] userClaims = + { + new Claim("Whatever", "Value"), + new Claim("Whatever2", "Value2") + }; + foreach (var c in userClaims) + { + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, c)); + } + } + + [Fact] + public async Task LoadFromDbFindByIdTest() + { + var user = CreateTestUser(); + await LazyLoadTestSetup(user); + + var manager = CreateManager(); + + var userById = await manager.FindByIdAsync(user.Id.ToString()); + Assert.Equal(2, (await manager.GetClaimsAsync(userById)).Count); + Assert.Equal(1, (await manager.GetLoginsAsync(userById)).Count); + Assert.Equal(2, (await manager.GetRolesAsync(userById)).Count); + } + + [Fact] + public async Task LoadFromDbFindByNameTest() + { + var user = CreateTestUser(); + await LazyLoadTestSetup(user); + var manager = CreateManager(); + var userByName = await manager.FindByNameAsync(user.UserName); + Assert.Equal(2, (await manager.GetClaimsAsync(userByName)).Count); + Assert.Equal(1, (await manager.GetLoginsAsync(userByName)).Count); + Assert.Equal(2, (await manager.GetRolesAsync(userByName)).Count); + } + + [Fact] + public async Task LoadFromDbFindByLoginTest() + { + var user = CreateTestUser(); + await LazyLoadTestSetup(user); + + var manager = CreateManager(); + var userByLogin = await manager.FindByLoginAsync("provider", user.Id.ToString()); + Assert.Equal(2, (await manager.GetClaimsAsync(userByLogin)).Count); + Assert.Equal(1, (await manager.GetLoginsAsync(userByLogin)).Count); + Assert.Equal(2, (await manager.GetRolesAsync(userByLogin)).Count); + } + + [Fact] + public async Task LoadFromDbFindByEmailTest() + { + var user = CreateTestUser(); + user.Email = "fooz@fizzy.pop"; + await LazyLoadTestSetup(user); + + var manager = CreateManager(); + var userByEmail = await manager.FindByEmailAsync(user.Email); + Assert.Equal(2, (await manager.GetClaimsAsync(userByEmail)).Count); + Assert.Equal(1, (await manager.GetLoginsAsync(userByEmail)).Count); + Assert.Equal(2, (await manager.GetRolesAsync(userByEmail)).Count); + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/UserStoreGuidKeyTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/UserStoreGuidKeyTest.cs new file mode 100644 index 0000000..565f7ed --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/UserStoreGuidKeyTest.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using AspNetCore.Identity.MongoDbCore.Models; +using AspNetCore.Identity.MongoDbCore; +using MongoDbGenericRepository; +using AspNetCore.Identity.MongoDbCore.IntegrationTests.Infrastructure; +using Microsoft.AspNetCore.Identity; + +namespace AspNetCore.Identity.MongoDbCore.Test +{ + public class GuidUser : MongoIdentityUser + { + public GuidUser() : base() + { + } + } + + public class GuidRole : MongoIdentityRole + { + public GuidRole() : base() + { + } + } + + public class UserStoreGuidTest : SqlStoreTestBase + { + public UserStoreGuidTest(MongoDatabaseFixture fixture) + : base(fixture) + { + } + + public class ApplicationUserStore : MongoUserStore + { + public ApplicationUserStore(IMongoDbContext context) : base(Container.MongoRepository.Context) { } + } + + public class ApplicationRoleStore : MongoRoleStore + { + public ApplicationRoleStore(IMongoDbContext context) : base(Container.MongoRepository.Context) { } + } + + protected override void AddUserStore(IServiceCollection services, object context = null) + { + services.AddSingleton>(new ApplicationUserStore(Container.MongoRepository.Context)); + } + + protected override void AddRoleStore(IServiceCollection services, object context = null) + { + services.AddSingleton>(new ApplicationRoleStore(Container.MongoRepository.Context)); + } + + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/UserStoreIntKeyTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/UserStoreIntKeyTest.cs new file mode 100644 index 0000000..f9ba8fe --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/UserStoreIntKeyTest.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using AspNetCore.Identity.MongoDbCore.Models; + +namespace AspNetCore.Identity.MongoDbCore.Test +{ + public class IntUser : MongoIdentityUser + { + public IntUser() : base() + { + } + } + + public class IntRole : MongoIdentityRole + { + public IntRole() : base() + { + Name = Guid.NewGuid().ToString(); + } + } + + public class UserStoreIntTest : SqlStoreTestBase + { + public UserStoreIntTest(MongoDatabaseFixture fixture) + : base(fixture) + { + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/UserStoreStringKeyTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/UserStoreStringKeyTest.cs new file mode 100644 index 0000000..5571bf4 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/UserStoreStringKeyTest.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using AspNetCore.Identity.MongoDbCore.Models; + +namespace AspNetCore.Identity.MongoDbCore.Test +{ + public class StringUser : MongoDbIdentityUser + { + public StringUser() : base() + { + } + } + + public class StringRole : MongoDbIdentityRole + { + public StringRole() : base() + { + } + } + + public class UserStoreStringKeyTest : SqlStoreTestBase + { + public UserStoreStringKeyTest(MongoDatabaseFixture fixture) + : base(fixture) + { } + + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/UserStoreTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/UserStoreTest.cs new file mode 100644 index 0000000..1d4f0f4 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/UserStoreTest.cs @@ -0,0 +1,364 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.Test; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using AspNetCore.Identity.MongoDbCore.Models; +using AspNetCore.Identity.MongoDbCore; +using MongoDB.Driver; +using AspNetCore.Identity.MongoDbCore.IntegrationTests.Infrastructure; +using AspNetCore.Identity.MongoDbCore.Infrastructure; +using MongoDbGenericRepository; +using Microsoft.AspNetCore.Identity; + +namespace AspNetCore.Identity.MongoDbCore.Test +{ + + public class ApplicationDbContext : MongoIdentityDbContext + { + public ApplicationDbContext(string connectionString, string databaseName) : base(connectionString, databaseName) + { + } + } + + public sealed class Applicationcontext + { + public static ApplicationDbContext Instance = new ApplicationDbContext( + Container.MongoDbIdentityConfiguration.MongoDbSettings.ConnectionString, + Container.MongoDbIdentityConfiguration.MongoDbSettings.DatabaseName); + } + + public class UserStoreTest : IdentitySpecificationTestBase, IClassFixture> + { + private readonly MongoDatabaseFixture _fixture; + + public UserStoreTest(MongoDatabaseFixture fixture) + { + _fixture = fixture; + } + + protected override bool ShouldSkipDbTests() + => false; + + [Fact] + public void CanCreateUserUsingEF() + { + var user = CreateTestUser(); + user.Id = Guid.NewGuid().ToString(); + var guidString = user.Id.ToString(); + user.UserName = guidString; + Container.MongoRepository.AddOne(user); + Assert.True(Container.MongoRepository.Any(u => u.UserName == guidString)); + Assert.NotNull(Container.MongoRepository.GetOne(u => u.UserName == guidString)); + } + + protected override void AddUserStore(IServiceCollection services, object context = null) + { + services.AddSingleton>(new MongoUserStore(Container.MongoRepository.Context)); + } + + protected override void AddRoleStore(IServiceCollection services, object context = null) + { + services.AddSingleton>(new MongoRoleStore(Container.MongoRepository.Context)); + } + + [Fact] + public async Task SqlUserStoreMethodsThrowWhenDisposedTest() + { + var store = new MongoUserStore(Container.MongoRepository.Context); + store.Dispose(); + await Assert.ThrowsAsync(async () => await store.AddClaimsAsync(null, null)); + await Assert.ThrowsAsync(async () => await store.AddLoginAsync(null, null)); + await Assert.ThrowsAsync(async () => await store.AddToRoleAsync(null, null)); + await Assert.ThrowsAsync(async () => await store.GetClaimsAsync(null)); + await Assert.ThrowsAsync(async () => await store.GetLoginsAsync(null)); + await Assert.ThrowsAsync(async () => await store.GetRolesAsync(null)); + await Assert.ThrowsAsync(async () => await store.IsInRoleAsync(null, null)); + await Assert.ThrowsAsync(async () => await store.RemoveClaimsAsync(null, null)); + await Assert.ThrowsAsync(async () => await store.RemoveLoginAsync(null, null, null)); + await Assert.ThrowsAsync( + async () => await store.RemoveFromRoleAsync(null, null)); + await Assert.ThrowsAsync(async () => await store.RemoveClaimsAsync(null, null)); + await Assert.ThrowsAsync(async () => await store.ReplaceClaimAsync(null, null, null)); + await Assert.ThrowsAsync(async () => await store.FindByLoginAsync(null, null)); + await Assert.ThrowsAsync(async () => await store.FindByIdAsync(null)); + await Assert.ThrowsAsync(async () => await store.FindByNameAsync(null)); + await Assert.ThrowsAsync(async () => await store.CreateAsync(null)); + await Assert.ThrowsAsync(async () => await store.UpdateAsync(null)); + await Assert.ThrowsAsync(async () => await store.DeleteAsync(null)); + await Assert.ThrowsAsync( + async () => await store.SetEmailConfirmedAsync(null, true)); + await Assert.ThrowsAsync(async () => await store.GetEmailConfirmedAsync(null)); + await Assert.ThrowsAsync( + async () => await store.SetPhoneNumberConfirmedAsync(null, true)); + await Assert.ThrowsAsync( + async () => await store.GetPhoneNumberConfirmedAsync(null)); + } + + [Fact] + public async Task UserStorePublicNullCheckTest() + { + Assert.Throws("context", () => new MongoUserStore(null)); + var store = new MongoUserStore(Container.MongoRepository.Context); + await Assert.ThrowsAsync("user", async () => await store.GetUserIdAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.GetUserNameAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.SetUserNameAsync(null, null)); + await Assert.ThrowsAsync("user", async () => await store.CreateAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.UpdateAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.DeleteAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.AddClaimsAsync(null, null)); + await Assert.ThrowsAsync("user", async () => await store.ReplaceClaimAsync(null, null, null)); + await Assert.ThrowsAsync("user", async () => await store.RemoveClaimsAsync(null, null)); + await Assert.ThrowsAsync("user", async () => await store.GetClaimsAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.GetLoginsAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.GetRolesAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.AddLoginAsync(null, null)); + await + Assert.ThrowsAsync("user", async () => await store.RemoveLoginAsync(null, null, null)); + await Assert.ThrowsAsync("user", async () => await store.AddToRoleAsync(null, null)); + await + Assert.ThrowsAsync("user", + async () => await store.RemoveFromRoleAsync(null, null)); + await Assert.ThrowsAsync("user", async () => await store.IsInRoleAsync(null, null)); + await Assert.ThrowsAsync("user", async () => await store.GetPasswordHashAsync(null)); + await + Assert.ThrowsAsync("user", + async () => await store.SetPasswordHashAsync(null, null)); + await Assert.ThrowsAsync("user", async () => await store.GetSecurityStampAsync(null)); + await Assert.ThrowsAsync("user", + async () => await store.SetSecurityStampAsync(null, null)); + await Assert.ThrowsAsync("login", async () => await store.AddLoginAsync(new MongoDbIdentityUser("fake"), null)); + await Assert.ThrowsAsync("claims", + async () => await store.AddClaimsAsync(new MongoDbIdentityUser("fake"), null)); + await Assert.ThrowsAsync("claims", + async () => await store.RemoveClaimsAsync(new MongoDbIdentityUser("fake"), null)); + await Assert.ThrowsAsync("user", async () => await store.GetEmailConfirmedAsync(null)); + await Assert.ThrowsAsync("user", + async () => await store.SetEmailConfirmedAsync(null, true)); + await Assert.ThrowsAsync("user", async () => await store.GetEmailAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.SetEmailAsync(null, null)); + await Assert.ThrowsAsync("user", async () => await store.GetPhoneNumberAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.SetPhoneNumberAsync(null, null)); + await Assert.ThrowsAsync("user", + async () => await store.GetPhoneNumberConfirmedAsync(null)); + await Assert.ThrowsAsync("user", + async () => await store.SetPhoneNumberConfirmedAsync(null, true)); + await Assert.ThrowsAsync("user", async () => await store.GetTwoFactorEnabledAsync(null)); + await Assert.ThrowsAsync("user", + async () => await store.SetTwoFactorEnabledAsync(null, true)); + await Assert.ThrowsAsync("user", async () => await store.GetAccessFailedCountAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.GetLockoutEnabledAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.SetLockoutEnabledAsync(null, false)); + await Assert.ThrowsAsync("user", async () => await store.GetLockoutEndDateAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.SetLockoutEndDateAsync(null, new DateTimeOffset())); + await Assert.ThrowsAsync("user", async () => await store.ResetAccessFailedCountAsync(null)); + await Assert.ThrowsAsync("user", async () => await store.IncrementAccessFailedCountAsync(null)); + await Assert.ThrowsAsync("normalizedRoleName", async () => await store.AddToRoleAsync(new MongoDbIdentityUser("fake"), null)); + await Assert.ThrowsAsync("normalizedRoleName", async () => await store.RemoveFromRoleAsync(new MongoDbIdentityUser("fake"), null)); + await Assert.ThrowsAsync("normalizedRoleName", async () => await store.IsInRoleAsync(new MongoDbIdentityUser("fake"), null)); + await Assert.ThrowsAsync("normalizedRoleName", async () => await store.AddToRoleAsync(new MongoDbIdentityUser("fake"), "")); + await Assert.ThrowsAsync("normalizedRoleName", async () => await store.RemoveFromRoleAsync(new MongoDbIdentityUser("fake"), "")); + await Assert.ThrowsAsync("normalizedRoleName", async () => await store.IsInRoleAsync(new MongoDbIdentityUser("fake"), "")); + } + + [Fact] + public async Task CanCreateUsingManager() + { + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.DeleteAsync(user)); + } + + [Fact] + public async Task TwoUsersSamePasswordDifferentHash() + { + var manager = CreateManager(); + var userA = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(userA, "password")); + var userB = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(userB, "password")); + + Assert.NotEqual(userA.PasswordHash, userB.PasswordHash); + } + + [Fact] + public async Task AddUserToUnknownRoleFails() + { + var manager = CreateManager(); + var u = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(u)); + await Assert.ThrowsAsync( + async () => await manager.AddToRoleAsync(u, "bogus")); + } + + [Fact] + public async Task ConcurrentUpdatesWillFail() + { + var user = CreateTestUser(); + var manager = CreateManager(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var manager1 = CreateManager(); + var manager2 = CreateManager(); + var user1 = await manager1.FindByIdAsync(user.Id); + var user2 = await manager2.FindByIdAsync(user.Id); + Assert.NotNull(user1); + Assert.NotNull(user2); + Assert.NotSame(user1, user2); + user1.UserName = Guid.NewGuid().ToString(); + user2.UserName = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(user1)); + IdentityResultAssert.IsFailure(await manager2.UpdateAsync(user2), new IdentityErrorDescriber().ConcurrencyFailure()); + } + + [Fact] + public async Task ConcurrentUpdatesWillFailWithDetachedUser() + { + var user = CreateTestUser(); + var manager = CreateManager(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var manager1 = CreateManager(); + var manager2 = CreateManager(); + var user2 = await manager2.FindByIdAsync(user.Id); + Assert.NotNull(user2); + Assert.NotSame(user, user2); + user.UserName = Guid.NewGuid().ToString(); + user2.UserName = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(user)); + IdentityResultAssert.IsFailure(await manager2.UpdateAsync(user2), new IdentityErrorDescriber().ConcurrencyFailure()); + } + + + [Fact] + public async Task DeleteAModifiedUserWillFail() + { + var user = CreateTestUser(); + + var manager = CreateManager(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + + var manager1 = CreateManager(); + var manager2 = CreateManager(); + var user1 = await manager1.FindByIdAsync(user.Id); + var user2 = await manager2.FindByIdAsync(user.Id); + Assert.NotNull(user1); + Assert.NotNull(user2); + Assert.NotSame(user1, user2); + user1.UserName = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(user1)); + IdentityResultAssert.IsFailure(await manager2.DeleteAsync(user2), new IdentityErrorDescriber().ConcurrencyFailure()); + } + + [Fact] + public async Task ConcurrentRoleUpdatesWillFail() + { + var role = CreateRole(); + + var manager = CreateRoleManager(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + + var manager1 = CreateRoleManager(); + var manager2 = CreateRoleManager(); + var role1 = await manager1.FindByIdAsync(role.Id); + var role2 = await manager2.FindByIdAsync(role.Id); + Assert.NotNull(role1); + Assert.NotNull(role2); + Assert.NotSame(role1, role2); + role1.Name = Guid.NewGuid().ToString(); + role2.Name = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(role1)); + IdentityResultAssert.IsFailure(await manager2.UpdateAsync(role2), new IdentityErrorDescriber().ConcurrencyFailure()); + } + + [Fact] + public async Task ConcurrentRoleUpdatesWillFailWithDetachedRole() + { + var role = CreateRole(); + var manager = CreateRoleManager(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + var manager1 = CreateRoleManager(); + var manager2 = CreateRoleManager(); + var role2 = await manager2.FindByIdAsync(role.Id); + Assert.NotNull(role); + Assert.NotNull(role2); + Assert.NotSame(role, role2); + role.Name = Guid.NewGuid().ToString(); + role2.Name = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(role)); + IdentityResultAssert.IsFailure(await manager2.UpdateAsync(role2), new IdentityErrorDescriber().ConcurrencyFailure()); + } + + private MongoDbIdentityRole CreateRole() + { + var guid = Guid.NewGuid().ToString(); + var role = new MongoDbIdentityRole(guid); + _fixture.RolesToDelete.Add(role); + return role; + } + + [Fact] + public async Task DeleteAModifiedRoleWillFail() + { + var role = CreateRole(); + var manager = CreateRoleManager(); + var result = await manager.CreateAsync(role); + + IdentityResultAssert.IsSuccess(result); + + var manager1 = CreateRoleManager(); + var manager2 = CreateRoleManager(true); + var role1 = await manager1.FindByIdAsync(role.Id); + var role2 = await manager2.FindByIdAsync(role.Id); + Assert.NotNull(role1); + Assert.NotNull(role2); + Assert.NotSame(role1, role2); + role1.Name = Guid.NewGuid().ToString(); + IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(role1)); + IdentityResultAssert.IsFailure(await manager2.DeleteAsync(role2), new IdentityErrorDescriber().ConcurrencyFailure()); + } + + protected override MongoDbIdentityUser CreateTestUser(string namePrefix = "", string email = "", string phoneNumber = "", + bool lockoutEnabled = false, DateTimeOffset? lockoutEnd = default(DateTimeOffset?), bool useNamePrefixAsUserName = false) + { + var user = new MongoDbIdentityUser + { + UserName = useNamePrefixAsUserName ? namePrefix : string.Format("{0}{1}", namePrefix, Guid.NewGuid()), + Email = email, + PhoneNumber = phoneNumber, + LockoutEnabled = lockoutEnabled, + LockoutEnd = lockoutEnd + }; + _fixture.UsersToDelete.Add(user); + return user; + } + + protected override MongoDbIdentityRole CreateTestRole(string roleNamePrefix = "", bool useRoleNamePrefixAsRoleName = false) + { + var roleName = useRoleNamePrefixAsRoleName ? roleNamePrefix : string.Format("{0}{1}", roleNamePrefix, Guid.NewGuid()); + var role = new MongoDbIdentityRole(roleName); + _fixture.RolesToDelete.Add(role); + return role; + } + + protected override void SetUserPasswordHash(MongoDbIdentityUser user, string hashedPassword) + { + user.PasswordHash = hashedPassword; + } + + protected override Expression> UserNameEqualsPredicate(string userName) => u => u.UserName == userName; + + protected override Expression> RoleNameEqualsPredicate(string roleName) => r => r.Name == roleName; + + protected override Expression> RoleNameStartsWithPredicate(string roleName) => r => r.Name.StartsWith(roleName); + + protected override Expression> UserNameStartsWithPredicate(string userName) => u => u.UserName.StartsWith(userName); + + } + + public class ApplicationUser : MongoIdentityUser { } +} diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/UserStoreWithGenericsTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/UserStoreWithGenericsTest.cs new file mode 100644 index 0000000..3f2e2a1 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/UserStoreWithGenericsTest.cs @@ -0,0 +1,353 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.Test; +//using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using AspNetCore.Identity.MongoDbCore.Models; +using AspNetCore.Identity.MongoDbCore; +using AspNetCore.Identity.MongoDbCore.IntegrationTests.Infrastructure; +using MongoDbGenericRepository; +using Microsoft.AspNetCore.Identity; + +namespace AspNetCore.Identity.MongoDbCore.Test +{ + public class UserStoreWithGenericsTest : IdentitySpecificationTestBase, IClassFixture> + { + private readonly MongoDatabaseFixture _fixture; + + public UserStoreWithGenericsTest(MongoDatabaseFixture fixture) + { + _fixture = fixture; + } + + protected override bool ShouldSkipDbTests() + { + return false; + } + + protected override void AddUserStore(IServiceCollection services, object context = null) + { + services.AddSingleton>(new MongoUserStore(Container.MongoRepository.Context)); + //services.AddSingleton>(new UserStoreWithGenerics((ContextWithGenerics)context, "TestContext")); + } + + protected override void AddRoleStore(IServiceCollection services, object context = null) + { + services.AddSingleton>(new MongoRoleStore(Container.MongoRepository.Context)); + //services.AddSingleton>(new RoleStoreWithGenerics(Container.MongoRepository.Context, "TestContext")); + } + + protected override IdentityUserWithGenerics CreateTestUser(string namePrefix = "", string email = "", string phoneNumber = "", + bool lockoutEnabled = false, DateTimeOffset? lockoutEnd = default(DateTimeOffset?), bool useNamePrefixAsUserName = false) + { + var user = new IdentityUserWithGenerics + { + UserName = useNamePrefixAsUserName ? namePrefix : string.Format("{0}{1}", namePrefix, Guid.NewGuid()), + Email = email, + PhoneNumber = phoneNumber, + LockoutEnabled = lockoutEnabled, + LockoutEnd = lockoutEnd + }; + _fixture.UsersToDelete.Add(user); + return user; + } + + protected override MyIdentityRole CreateTestRole(string roleNamePrefix = "", bool useRoleNamePrefixAsRoleName = false) + { + var roleName = useRoleNamePrefixAsRoleName ? roleNamePrefix : string.Format("{0}{1}", roleNamePrefix, Guid.NewGuid()); + var role = new MyIdentityRole(roleName); + _fixture.RolesToDelete.Add(role); + return role; + } + + protected override void SetUserPasswordHash(IdentityUserWithGenerics user, string hashedPassword) + { + user.PasswordHash = hashedPassword; + } + + protected override Expression> UserNameEqualsPredicate(string userName) => u => u.UserName == userName; + + protected override Expression> RoleNameEqualsPredicate(string roleName) => r => r.Name == roleName; + + protected override Expression> UserNameStartsWithPredicate(string userName) => u => u.UserName.StartsWith(userName); + + protected override Expression> RoleNameStartsWithPredicate(string roleName) => r => r.Name.StartsWith(roleName); + + [Fact] + public void AddEntityFrameworkStoresWithInvalidUserThrows() + { + var services = new ServiceCollection(); + var builder = services.AddIdentity(); + var e = Assert.Throws(() => + //builder.AddEntityFrameworkStores() + Throw() + ); + Assert.Contains("AddEntityFrameworkStores", e.Message); + } + + private void Throw() + { + throw new InvalidOperationException("AddEntityFrameworkStores"); + } + + [Fact] + public void AddEntityFrameworkStoresWithInvalidRoleThrows() + { + var services = new ServiceCollection(); + var builder = services.AddIdentity(); + var e = Assert.Throws(() => { + //builder.AddEntityFrameworkStores(); + Throw(); + }); + Assert.Contains("AddEntityFrameworkStores", e.Message); + } + + [Fact] + public async Task CanAddRemoveUserClaimWithIssuer() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + Claim[] claims = { new Claim("c1", "v1", null, "i1"), new Claim("c2", "v2", null, "i2"), new Claim("c2", "v3", null, "i3") }; + foreach (Claim c in claims) + { + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, c)); + } + + var userId = await manager.GetUserIdAsync(user); + var userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(3, userClaims.Count); + Assert.Equal(3, userClaims.Intersect(claims, ClaimEqualityComparer.Default).Count()); + + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[0])); + userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(2, userClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[1])); + userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(1, userClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[2])); + userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(0, userClaims.Count); + } + + [Fact] + public async Task RemoveClaimWithIssuerOnlyAffectsUser() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + var user2 = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user2)); + Claim[] claims = { new Claim("c", "v", null, "i1"), new Claim("c2", "v2", null, "i2"), new Claim("c2", "v3", null, "i3") }; + foreach (Claim c in claims) + { + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, c)); + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user2, c)); + } + var userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(3, userClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[0])); + userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(2, userClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[1])); + userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(1, userClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[2])); + userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(0, userClaims.Count); + var userClaims2 = await manager.GetClaimsAsync(user2); + Assert.Equal(3, userClaims2.Count); + } + + [Fact] + public async Task CanReplaceUserClaimWithIssuer() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, new Claim("c", "a", "i"))); + var userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(1, userClaims.Count); + Claim claim = new Claim("c", "b", "i"); + Claim oldClaim = userClaims.FirstOrDefault(); + IdentityResultAssert.IsSuccess(await manager.ReplaceClaimAsync(user, oldClaim, claim)); + var newUserClaims = await manager.GetClaimsAsync(user); + Assert.Equal(1, newUserClaims.Count); + Claim newClaim = newUserClaims.FirstOrDefault(); + Assert.Equal(claim.Type, newClaim.Type); + Assert.Equal(claim.Value, newClaim.Value); + Assert.Equal(claim.Issuer, newClaim.Issuer); + } + } + + public class ClaimEqualityComparer : IEqualityComparer + { + public static IEqualityComparer Default = new ClaimEqualityComparer(); + + public bool Equals(Claim x, Claim y) + { + return x.Value == y.Value && x.Type == y.Type && x.Issuer == y.Issuer; + } + + public int GetHashCode(Claim obj) + { + return 1; + } + } + + + #region Generic Type defintions + + public class IdentityUserWithGenerics : MongoDbIdentityUser + { + public IdentityUserWithGenerics() : base() + { + + } + } + + public class UserStoreWithGenerics : MongoUserStore + { + public string LoginContext { get; set; } + + public UserStoreWithGenerics(IMongoDbContext context, string loginContext) : base(context) + { + LoginContext = loginContext; + } + + protected override IdentityUserRoleWithDate CreateUserRole(IdentityUserWithGenerics user, MyIdentityRole role) + { + return new IdentityUserRoleWithDate() + { + RoleId = role.Id, + UserId = user.Id, + Created = DateTime.UtcNow + }; + } + + protected override IdentityUserClaimWithIssuer CreateUserClaim(IdentityUserWithGenerics user, Claim claim) + { + return new IdentityUserClaimWithIssuer { UserId = user.Id, ClaimType = claim.Type, ClaimValue = claim.Value, Issuer = claim.Issuer }; + } + + protected override IdentityUserLoginWithContext CreateUserLogin(IdentityUserWithGenerics user, UserLoginInfo login) + { + return new IdentityUserLoginWithContext + { + UserId = user.Id, + ProviderKey = login.ProviderKey, + LoginProvider = login.LoginProvider, + ProviderDisplayName = login.ProviderDisplayName, + Context = LoginContext + }; + } + + protected override IdentityUserTokenWithStuff CreateUserToken(IdentityUserWithGenerics user, string loginProvider, string name, string value) + { + return new IdentityUserTokenWithStuff + { + UserId = user.Id, + LoginProvider = loginProvider, + Name = name, + Value = value, + Stuff = "stuff" + }; + } + } + + public class RoleStoreWithGenerics : MongoRoleStore + { + private string _loginContext; + public RoleStoreWithGenerics(IMongoDbContext context, string loginContext) : base(context) + { + _loginContext = loginContext; + } + + protected override IdentityRoleClaimWithIssuer CreateRoleClaim(MyIdentityRole role, Claim claim) + { + return new IdentityRoleClaimWithIssuer { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value, Issuer = claim.Issuer }; + } + } + + public class IdentityUserClaimWithIssuer : IdentityUserClaim + { + public string Issuer { get; set; } + + public override Claim ToClaim() + { + return new Claim(ClaimType, ClaimValue, null, Issuer); + } + + public override void InitializeFromClaim(Claim other) + { + ClaimValue = other.Value; + ClaimType = other.Type; + Issuer = other.Issuer; + } + } + + public class IdentityRoleClaimWithIssuer : IdentityRoleClaim + { + public string Issuer { get; set; } + + public override Claim ToClaim() + { + return new Claim(ClaimType, ClaimValue, null, Issuer); + } + + public override void InitializeFromClaim(Claim other) + { + ClaimValue = other.Value; + ClaimType = other.Type; + Issuer = other.Issuer; + } + } + + public class IdentityUserRoleWithDate : IdentityUserRole + { + public DateTime Created { get; set; } + } + + public class MyIdentityRole : MongoDbIdentityRole + { + public MyIdentityRole() : base() + { + } + + public MyIdentityRole(string roleName) : base(roleName) + { + } + } + + public class IdentityUserTokenWithStuff : IdentityUserToken + { + public string Stuff { get; set; } + } + + public class IdentityUserLoginWithContext : IdentityUserLogin + { + public string Context { get; set; } + } + + #endregion +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/Utilities/MongoDatabaseFixture.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/Utilities/MongoDatabaseFixture.cs new file mode 100644 index 0000000..ab42c7e --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/Utilities/MongoDatabaseFixture.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using MongoDbGenericRepository; +using AspNetCore.Identity.MongoDbCore.IntegrationTests.Infrastructure; +using System.Collections.Concurrent; +using System.Linq; +using MongoDB.Driver; +using MongoDbGenericRepository.Models; + +namespace AspNetCore.Identity.MongoDbCore.Test +{ + public class MongoDatabaseFixture : IDisposable + where TUser : IDocument + where TKey : IEquatable + { + + public IMongoDbContext Context; + + public MongoDatabaseFixture() + { + Context = new MongoDbContext( + Container.MongoDbIdentityConfiguration.MongoDbSettings.ConnectionString, + Container.MongoDbIdentityConfiguration.MongoDbSettings.DatabaseName); + UsersToDelete = new ConcurrentBag(); + } + public ConcurrentBag UsersToDelete { get; set; } + public virtual void Dispose() + { + var userIds = UsersToDelete.ToList().Select(e => e.Id); + if (userIds.Any()) + { + Context.GetCollection().DeleteMany(e => userIds.Contains(e.Id)); + } + } + } + + public class MongoDatabaseFixture : MongoDatabaseFixture, IDisposable + where TUser : IDocument + where TRole : IDocument + where TKey : IEquatable + { + + public MongoDatabaseFixture() + { + Context = new MongoDbContext( + Container.MongoDbIdentityConfiguration.MongoDbSettings.ConnectionString, + Container.MongoDbIdentityConfiguration.MongoDbSettings.DatabaseName); + UsersToDelete = new ConcurrentBag(); + RolesToDelete = new ConcurrentBag(); + } + public ConcurrentBag RolesToDelete { get; set; } + + public override void Dispose() + { + var userIds = UsersToDelete.ToList().Select(e => e.Id); + if (userIds.Any()) + { + Context.GetCollection().DeleteMany(e => userIds.Contains(e.Id)); + } + var roleIds = RolesToDelete.ToList().Select(e => e.Id); + if (roleIds.Any()) + { + Context.GetCollection().DeleteMany(e => roleIds.Contains(e.Id)); + } + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/Utilities/SqlServerTestStore.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/Utilities/SqlServerTestStore.cs new file mode 100644 index 0000000..897f7a8 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.MongoDbCore.Test/Utilities/SqlServerTestStore.cs @@ -0,0 +1,165 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Data.Common; +using System.Data.SqlClient; +using System.IO; +using System.Threading; + +namespace AspNetCore.Identity.MongoDbCore.Test.Utilities +{ + //public class SqlServerTestStore : IDisposable + //{ + // public const int CommandTimeout = 90; + + // public static string CreateConnectionString(string name) + // { + // var connStrBuilder = new SqlConnectionStringBuilder(TestEnvironment.Config["Test:SqlServer:DefaultConnectionString"]) + // { + // InitialCatalog = name + // }; + + // return connStrBuilder.ConnectionString; + // } + + // public static SqlServerTestStore CreateScratch(bool createDatabase = true) + // => new SqlServerTestStore(GetScratchDbName()).CreateTransient(createDatabase); + + // private SqlConnection _connection; + // private readonly string _name; + // private bool _deleteDatabase; + + // private SqlServerTestStore(string name) + // { + // _name = name; + // } + + // private static string GetScratchDbName() + // { + // string name; + // do + // { + // name = "Scratch_" + Guid.NewGuid(); + // } while (DatabaseExists(name) + // || DatabaseFilesExist(name)); + + // return name; + // } + + // private static void WaitForExists(SqlConnection connection) + // { + // var retryCount = 0; + // while (true) + // { + // try + // { + // connection.Open(); + + // connection.Close(); + + // return; + // } + // catch (SqlException e) + // { + // if (++retryCount >= 30 + // || (e.Number != 233 && e.Number != -2 && e.Number != 4060)) + // { + // throw; + // } + + // SqlConnection.ClearPool(connection); + + // Thread.Sleep(100); + // } + // } + // } + + // private SqlServerTestStore CreateTransient(bool createDatabase) + // { + // _connection = new SqlConnection(CreateConnectionString(_name)); + + // if (createDatabase) + // { + // using (var master = new SqlConnection(CreateConnectionString("master"))) + // { + // master.Open(); + // using (var command = master.CreateCommand()) + // { + // command.CommandTimeout = CommandTimeout; + // command.CommandText = $"{Environment.NewLine}CREATE DATABASE [{_name}]"; + + // command.ExecuteNonQuery(); + + // WaitForExists(_connection); + // } + // } + // _connection.Open(); + // } + + // _deleteDatabase = true; + // return this; + // } + + // private static bool DatabaseExists(string name) + // { + // using (var master = new SqlConnection(CreateConnectionString("master"))) + // { + // master.Open(); + + // using (var command = master.CreateCommand()) + // { + // command.CommandTimeout = CommandTimeout; + // command.CommandText = $@"SELECT COUNT(*) FROM sys.databases WHERE name = N'{name}'"; + + // return (int) command.ExecuteScalar() > 0; + // } + // } + // } + + // private static bool DatabaseFilesExist(string name) + // { + // var userFolder = Environment.GetEnvironmentVariable("USERPROFILE") ?? + // Environment.GetEnvironmentVariable("HOME"); + // return userFolder != null + // && (File.Exists(Path.Combine(userFolder, name + ".mdf")) + // || File.Exists(Path.Combine(userFolder, name + "_log.ldf"))); + // } + + // private void DeleteDatabase(string name) + // { + // using (var master = new SqlConnection(CreateConnectionString("master"))) + // { + // master.Open(); + + // using (var command = master.CreateCommand()) + // { + // command.CommandTimeout = CommandTimeout; + // // Query will take a few seconds if (and only if) there are active connections + + // // SET SINGLE_USER will close any open connections that would prevent the drop + // command.CommandText + // = string.Format(@"IF EXISTS (SELECT * FROM sys.databases WHERE name = N'{0}') + // BEGIN + // ALTER DATABASE [{0}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; + // DROP DATABASE [{0}]; + // END", name); + + // command.ExecuteNonQuery(); + // } + // } + // } + + // public DbConnection Connection => _connection; + + // public void Dispose() + // { + // _connection.Dispose(); + + // if (_deleteDatabase) + // { + // DeleteDatabase(_name); + // } + // } + //} +} diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/ApiConsistencyTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/ApiConsistencyTest.cs new file mode 100644 index 0000000..5df7e4e --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/ApiConsistencyTest.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class ApiConsistencyTest : ApiConsistencyTestBase + { + protected override Assembly TargetAssembly => typeof(IdentityOptions).GetTypeInfo().Assembly; + } +} diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/IdentityBuilderTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/IdentityBuilderTest.cs new file mode 100644 index 0000000..0c3b8c7 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/IdentityBuilderTest.cs @@ -0,0 +1,327 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class IdentityBuilderTest + { + + [Fact] + public void CanOverrideUserStore() + { + var services = new ServiceCollection() + .AddSingleton(new ConfigurationBuilder().Build()); + services.AddIdentity().AddUserStore(); + var thingy = services.BuildServiceProvider().GetRequiredService>() as MyUberThingy; + Assert.NotNull(thingy); + } + + [Fact] + public void CanOverrideRoleStore() + { + var services = new ServiceCollection() + .AddSingleton(new ConfigurationBuilder().Build()); + services.AddIdentity().AddRoleStore(); + var thingy = services.BuildServiceProvider().GetRequiredService>() as MyUberThingy; + Assert.NotNull(thingy); + } + + [Fact] + public void CanOverridePrincipalFactory() + { + var services = new ServiceCollection() + .AddLogging() + .AddSingleton(new ConfigurationBuilder().Build()); + services.AddIdentity() + .AddClaimsPrincipalFactory() + .AddUserManager() + .AddUserStore() + .AddRoleStore(); + var thingy = services.BuildServiceProvider().GetRequiredService>() as MyClaimsPrincipalFactory; + Assert.NotNull(thingy); + } + + [Fact] + public void CanOverrideRoleValidator() + { + var services = new ServiceCollection() + .AddSingleton(new ConfigurationBuilder().Build()); + services.AddIdentity().AddRoleValidator(); + var thingy = services.BuildServiceProvider().GetRequiredService>() as MyUberThingy; + Assert.NotNull(thingy); + } + + [Fact] + public void CanOverrideUserValidator() + { + var services = new ServiceCollection() + .AddSingleton(new ConfigurationBuilder().Build()); + services.AddIdentity().AddUserValidator(); + var thingy = services.BuildServiceProvider().GetRequiredService>() as MyUberThingy; + Assert.NotNull(thingy); + } + + [Fact] + public void CanOverridePasswordValidator() + { + var services = new ServiceCollection() + .AddSingleton(new ConfigurationBuilder().Build()); + services.AddIdentity().AddPasswordValidator(); + var thingy = services.BuildServiceProvider().GetRequiredService>() as MyUberThingy; + Assert.NotNull(thingy); + } + + [Fact] + public void CanOverrideUserManager() + { + var services = new ServiceCollection() + .AddSingleton(new ConfigurationBuilder().Build()); + services.AddIdentity() + .AddUserStore() + .AddUserManager(); + var myUserManager = services.BuildServiceProvider().GetRequiredService(typeof(UserManager)) as MyUserManager; + Assert.NotNull(myUserManager); + } + + [Fact] + public void CanOverrideRoleManager() + { + var services = new ServiceCollection() + .AddSingleton(new ConfigurationBuilder().Build()); + services.AddIdentity() + .AddRoleStore() + .AddRoleManager(); + var myRoleManager = services.BuildServiceProvider().GetRequiredService>() as MyRoleManager; + Assert.NotNull(myRoleManager); + } + + [Fact] + public void CanOverrideSignInManager() + { + var services = new ServiceCollection() + .AddSingleton(new ConfigurationBuilder().Build()) + .AddSingleton() + .AddLogging(); + services.AddIdentity() + .AddUserStore() + .AddRoleStore() + .AddUserManager() + .AddClaimsPrincipalFactory() + .AddSignInManager(); + var myUserManager = services.BuildServiceProvider().GetRequiredService(typeof(SignInManager)) as MySignInManager; + Assert.NotNull(myUserManager); + } + + [Fact] + public void EnsureDefaultServices() + { + var services = new ServiceCollection() + .AddSingleton(new ConfigurationBuilder().Build()); + services.AddIdentity(); + + var provider = services.BuildServiceProvider(); + var userValidator = provider.GetRequiredService>() as UserValidator; + Assert.NotNull(userValidator); + + var pwdValidator = provider.GetRequiredService>() as PasswordValidator; + Assert.NotNull(pwdValidator); + + var hasher = provider.GetRequiredService>() as PasswordHasher; + Assert.NotNull(hasher); + } + + [Fact] + public void EnsureDefaultTokenProviders() + { + var services = new ServiceCollection() + .AddSingleton(new ConfigurationBuilder().Build()); + services.AddIdentity().AddDefaultTokenProviders(); + + var provider = services.BuildServiceProvider(); + var tokenProviders = provider.GetRequiredService>().Value.Tokens.ProviderMap.Values; + Assert.Equal(4, tokenProviders.Count()); + } + + [Fact] + public void AddManagerWithWrongTypesThrows() + { + var services = new ServiceCollection() + .AddSingleton(new ConfigurationBuilder().Build()); + var builder = services.AddIdentity(); + Assert.Throws(() => builder.AddUserManager()); + Assert.Throws(() => builder.AddRoleManager()); + Assert.Throws(() => builder.AddSignInManager()); + } + + [Fact] + public void AddTokenProviderWithWrongTypesThrows() + { + var services = new ServiceCollection() + .AddSingleton(new ConfigurationBuilder().Build()); + var builder = services.AddIdentity(); + Assert.Throws(() => builder.AddTokenProvider("whatevs")); + Assert.Throws(() => builder.AddTokenProvider("whatevs", typeof(object))); + } + + private class MyUberThingy : IUserValidator, IPasswordValidator, IRoleValidator, IUserStore, IRoleStore + { + public Task CreateAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task CreateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + throw new NotImplementedException(); + } + + public Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetNormalizedRoleNameAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetNormalizedUserNameAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetRoleIdAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetRoleNameAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetUserIdAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetUserNameAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedRoleNameAsync(TestRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedUserNameAsync(TestUser user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetRoleNameAsync(TestRole role, string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetUserNameAsync(TestUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task UpdateAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task UpdateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task ValidateAsync(RoleManager manager, TestRole role) + { + throw new NotImplementedException(); + } + + public Task ValidateAsync(UserManager manager, TestUser user) + { + throw new NotImplementedException(); + } + + public Task ValidateAsync(UserManager manager, TestUser user, string password) + { + throw new NotImplementedException(); + } + + Task IRoleStore.FindByIdAsync(string roleId, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + Task IRoleStore.FindByNameAsync(string roleName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } + + private class MySignInManager : SignInManager + { + public MySignInManager(UserManager manager, IHttpContextAccessor context, IUserClaimsPrincipalFactory claimsFactory) : base(manager, context, claimsFactory, null, null, null) { } + } + + private class MyUserManager : UserManager + { + public MyUserManager(IUserStore store) : base(store, null, null, null, null, null, null, null, null) { } + } + + private class MyClaimsPrincipalFactory : UserClaimsPrincipalFactory + { + public MyClaimsPrincipalFactory(UserManager userManager, RoleManager roleManager, IOptions optionsAccessor) : base(userManager, roleManager, optionsAccessor) + { + } + } + + private class MyRoleManager : RoleManager + { + public MyRoleManager(IRoleStore store, + IEnumerable> roleValidators) : base(store, null, null, null, null) + { + + } + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/IdentityOptionsTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/IdentityOptionsTest.cs new file mode 100644 index 0000000..d5dde41 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/IdentityOptionsTest.cs @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class IdentityOptionsTest + { + [Fact] + public void VerifyDefaultOptions() + { + var options = new IdentityOptions(); + Assert.True(options.Lockout.AllowedForNewUsers); + Assert.Equal(TimeSpan.FromMinutes(5), options.Lockout.DefaultLockoutTimeSpan); + Assert.Equal(5, options.Lockout.MaxFailedAccessAttempts); + + Assert.True(options.Password.RequireDigit); + Assert.True(options.Password.RequireLowercase); + Assert.True(options.Password.RequireNonAlphanumeric); + Assert.True(options.Password.RequireUppercase); + Assert.Equal(6, options.Password.RequiredLength); + Assert.Equal(1, options.Password.RequiredUniqueChars); + + Assert.Equal("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+", options.User.AllowedUserNameCharacters); + Assert.False(options.User.RequireUniqueEmail); + + Assert.Equal(ClaimTypes.Role, options.ClaimsIdentity.RoleClaimType); + Assert.Equal(ClaimTypes.Name, options.ClaimsIdentity.UserNameClaimType); + Assert.Equal(ClaimTypes.NameIdentifier, options.ClaimsIdentity.UserIdClaimType); + Assert.Equal("AspNet.Identity.SecurityStamp", options.ClaimsIdentity.SecurityStampClaimType); + } + + [Fact] + public void CanCustomizeIdentityOptions() + { + var services = new ServiceCollection().Configure(options => options.Password.RequiredLength = -1); + services.AddIdentity(); + var serviceProvider = services.BuildServiceProvider(); + + var setup = serviceProvider.GetRequiredService>(); + Assert.NotNull(setup); + var optionsGetter = serviceProvider.GetRequiredService>(); + Assert.NotNull(optionsGetter); + var myOptions = optionsGetter.Value; + Assert.True(myOptions.Password.RequireLowercase); + Assert.True(myOptions.Password.RequireDigit); + Assert.True(myOptions.Password.RequireNonAlphanumeric); + Assert.True(myOptions.Password.RequireUppercase); + Assert.Equal(1, myOptions.Password.RequiredUniqueChars); + Assert.Equal(-1, myOptions.Password.RequiredLength); + } + + [Fact] + public void CanSetupIdentityOptions() + { + var services = new ServiceCollection(); + services.AddIdentity(options => options.User.RequireUniqueEmail = true); + var serviceProvider = services.BuildServiceProvider(); + + var optionsGetter = serviceProvider.GetRequiredService>(); + Assert.NotNull(optionsGetter); + + var myOptions = optionsGetter.Value; + Assert.True(myOptions.User.RequireUniqueEmail); + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/IdentityResultTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/IdentityResultTest.cs new file mode 100644 index 0000000..4b0065e --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/IdentityResultTest.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class IdentityResultTest + { + [Fact] + public void VerifyDefaultConstructor() + { + var result = new IdentityResult(); + Assert.False(result.Succeeded); + Assert.Empty(result.Errors); + } + + [Fact] + public void NullFailedUsesEmptyErrors() + { + var result = IdentityResult.Failed(); + Assert.False(result.Succeeded); + Assert.Empty(result.Errors); + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/NoopRoleStore.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/NoopRoleStore.cs new file mode 100644 index 0000000..087690b --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/NoopRoleStore.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class NoopRoleStore : IRoleStore + { + public Task CreateAsync(TestRole user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(IdentityResult.Success); + } + + public Task UpdateAsync(TestRole user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(IdentityResult.Success); + } + + public Task GetRoleNameAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public Task SetRoleNameAsync(TestRole role, string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task FindByIdAsync(string roleId, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public Task FindByNameAsync(string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public void Dispose() + { + } + + public Task DeleteAsync(TestRole user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(IdentityResult.Success); + } + + public Task GetRoleIdAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public Task GetNormalizedRoleNameAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public Task SetNormalizedRoleNameAsync(TestRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/NoopUserStore.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/NoopUserStore.cs new file mode 100644 index 0000000..bb30ed6 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/NoopUserStore.cs @@ -0,0 +1,66 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class NoopUserStore : IUserStore + { + public Task GetUserIdAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(user.Id); + } + + public Task GetUserNameAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(user.UserName); + } + + public Task SetUserNameAsync(TestUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task CreateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(IdentityResult.Success); + } + + public Task UpdateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(IdentityResult.Success); + } + + public Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public Task FindByNameAsync(string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public void Dispose() + { + } + + public Task DeleteAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(IdentityResult.Success); + } + + public Task GetNormalizedUserNameAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public Task SetNormalizedUserNameAsync(TestUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/PasswordHasherTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/PasswordHasherTest.cs new file mode 100644 index 0000000..768249d --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/PasswordHasherTest.cs @@ -0,0 +1,157 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Cryptography; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class PasswordHasherTest + { + [Fact] + public void Ctor_InvalidCompatMode_Throws() + { + // Act & assert + var ex = Assert.Throws(() => + { + new PasswordHasher(compatMode: (PasswordHasherCompatibilityMode)(-1)); + }); + Assert.Equal("The provided PasswordHasherCompatibilityMode is invalid.", ex.Message); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + public void Ctor_InvalidIterCount_Throws(int iterCount) + { + // Act & assert + var ex = Assert.Throws(() => + { + new PasswordHasher(iterCount: iterCount); + }); + Assert.Equal("The iteration count must be a positive integer.", ex.Message); + } + + [Theory] + [InlineData(PasswordHasherCompatibilityMode.IdentityV2)] + [InlineData(PasswordHasherCompatibilityMode.IdentityV3)] + public void FullRoundTrip(PasswordHasherCompatibilityMode compatMode) + { + // Arrange + var hasher = new PasswordHasher(compatMode: compatMode); + + // Act & assert - success case + var hashedPassword = hasher.HashPassword(null, "password 1"); + var successResult = hasher.VerifyHashedPassword(null, hashedPassword, "password 1"); + Assert.Equal(PasswordVerificationResult.Success, successResult); + + // Act & assert - failure case + var failedResult = hasher.VerifyHashedPassword(null, hashedPassword, "password 2"); + Assert.Equal(PasswordVerificationResult.Failed, failedResult); + } + + [Theory] + // Version 2 payloads + [InlineData("AAABAgMEBQYHCAkKCwwNDg+uAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALtH1uJg==")] // incorrect password + [InlineData("AAABAgMEBQYHCAkKCwwNDg+ukCEMDf0yyQ29NYubggE=")] // too short + [InlineData("AAABAgMEBQYHCAkKCwwNDg+ukCEMDf0yyQ29NYubggHIVY0sdEUfdyeM+E1LtH1uJgAAAAAAAAAAAAA=")] // extra data at end + // Version 3 payloads + [InlineData("AQAAAAAAAAD6AAAAEAhftMyfTJyAAAAAAAAAAAAAAAAAAAih5WsjXaR3PA9M")] // incorrect password + [InlineData("AQAAAAIAAAAyAAAAEOMwvh3+FZxqkdMBz2ekgGhwQ4A=")] // too short + [InlineData("AQAAAAIAAAAyAAAAEOMwvh3+FZxqkdMBz2ekgGhwQ4B6pZWND6zgESBuWiHwAAAAAAAAAAAA")] // extra data at end + public void VerifyHashedPassword_FailureCases(string hashedPassword) + { + // Arrange + var hasher = new PasswordHasher(); + + // Act + var result = hasher.VerifyHashedPassword(null, hashedPassword, "my password"); + + // Assert + Assert.Equal(PasswordVerificationResult.Failed, result); + } + + [Theory] + // Version 2 payloads + [InlineData("ANXrDknc7fGPpigibZXXZFMX4aoqz44JveK6jQuwY3eH/UyPhvr5xTPeGYEckLxz9A==")] // SHA1, 1000 iterations, 128-bit salt, 256-bit subkey + // Version 3 payloads + [InlineData("AQAAAAIAAAAyAAAAEOMwvh3+FZxqkdMBz2ekgGhwQ4B6pZWND6zgESBuWiHw")] // SHA512, 50 iterations, 128-bit salt, 128-bit subkey + [InlineData("AQAAAAIAAAD6AAAAIJbVi5wbMR+htSfFp8fTw8N8GOS/Sje+S/4YZcgBfU7EQuqv4OkVYmc4VJl9AGZzmRTxSkP7LtVi9IWyUxX8IAAfZ8v+ZfhjCcudtC1YERSqE1OEdXLW9VukPuJWBBjLuw==")] // SHA512, 250 iterations, 256-bit salt, 512-bit subkey + [InlineData("AQAAAAAAAAD6AAAAEAhftMyfTJylOlZT+eEotFXd1elee8ih5WsjXaR3PA9M")] // SHA1, 250 iterations, 128-bit salt, 128-bit subkey + [InlineData("AQAAAAEAA9CQAAAAIESkQuj2Du8Y+kbc5lcN/W/3NiAZFEm11P27nrSN5/tId+bR1SwV8CO1Jd72r4C08OLvplNlCDc3oQZ8efcW+jQ=")] // SHA256, 250000 iterations, 256-bit salt, 256-bit subkey + public void VerifyHashedPassword_Version2CompatMode_SuccessCases(string hashedPassword) + { + // Arrange + var hasher = new PasswordHasher(compatMode: PasswordHasherCompatibilityMode.IdentityV2); + + // Act + var result = hasher.VerifyHashedPassword(null, hashedPassword, "my password"); + + // Assert + Assert.Equal(PasswordVerificationResult.Success, result); + } + + [Theory] + // Version 2 payloads + [InlineData("ANXrDknc7fGPpigibZXXZFMX4aoqz44JveK6jQuwY3eH/UyPhvr5xTPeGYEckLxz9A==", PasswordVerificationResult.SuccessRehashNeeded)] // SHA1, 1000 iterations, 128-bit salt, 256-bit subkey + // Version 3 payloads + [InlineData("AQAAAAIAAAAyAAAAEOMwvh3+FZxqkdMBz2ekgGhwQ4B6pZWND6zgESBuWiHw", PasswordVerificationResult.SuccessRehashNeeded)] // SHA512, 50 iterations, 128-bit salt, 128-bit subkey + [InlineData("AQAAAAIAAAD6AAAAIJbVi5wbMR+htSfFp8fTw8N8GOS/Sje+S/4YZcgBfU7EQuqv4OkVYmc4VJl9AGZzmRTxSkP7LtVi9IWyUxX8IAAfZ8v+ZfhjCcudtC1YERSqE1OEdXLW9VukPuJWBBjLuw==", PasswordVerificationResult.SuccessRehashNeeded)] // SHA512, 250 iterations, 256-bit salt, 512-bit subkey + [InlineData("AQAAAAAAAAD6AAAAEAhftMyfTJylOlZT+eEotFXd1elee8ih5WsjXaR3PA9M", PasswordVerificationResult.SuccessRehashNeeded)] // SHA1, 250 iterations, 128-bit salt, 128-bit subkey + [InlineData("AQAAAAEAA9CQAAAAIESkQuj2Du8Y+kbc5lcN/W/3NiAZFEm11P27nrSN5/tId+bR1SwV8CO1Jd72r4C08OLvplNlCDc3oQZ8efcW+jQ=", PasswordVerificationResult.Success)] // SHA256, 250000 iterations, 256-bit salt, 256-bit subkey + public void VerifyHashedPassword_Version3CompatMode_SuccessCases(string hashedPassword, PasswordVerificationResult expectedResult) + { + // Arrange + var hasher = new PasswordHasher(compatMode: PasswordHasherCompatibilityMode.IdentityV3); + + // Act + var actualResult = hasher.VerifyHashedPassword(null, hashedPassword, "my password"); + + // Assert + Assert.Equal(expectedResult, actualResult); + } + + private sealed class PasswordHasher : PasswordHasher + { + public PasswordHasher(PasswordHasherCompatibilityMode? compatMode = null, int? iterCount = null) + : base(BuildOptions(compatMode, iterCount)) + { + } + + private static IOptions BuildOptions(PasswordHasherCompatibilityMode? compatMode, int? iterCount) + { + var options = new PasswordHasherOptionsAccessor(); + if (compatMode != null) + { + options.Value.CompatibilityMode = (PasswordHasherCompatibilityMode)compatMode; + } + if (iterCount != null) + { + options.Value.IterationCount = (int)iterCount; + } + return options; + } + } + + private sealed class SequentialRandomNumberGenerator : RandomNumberGenerator + { + private byte _value; + + public override void GetBytes(byte[] data) + { + for (int i = 0; i < data.Length; i++) + { + data[i] = _value++; + } + } + } + + private class PasswordHasherOptionsAccessor : IOptions + { + public PasswordHasherOptions Value { get; } = new PasswordHasherOptions(); + } + + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/PasswordValidatorTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/PasswordValidatorTest.cs new file mode 100644 index 0000000..36897ad --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/PasswordValidatorTest.cs @@ -0,0 +1,190 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class PasswordValidatorTest + { + [Flags] + public enum Errors + { + None = 0, + Length = 2, + Alpha = 4, + Upper = 8, + Lower = 16, + Digit = 32, + } + + [Fact] + public async Task ValidateThrowsWithNullTest() + { + // Setup + var validator = new PasswordValidator(); + + // Act + // Assert + await Assert.ThrowsAsync("password", () => validator.ValidateAsync(null, null, null)); + await Assert.ThrowsAsync("manager", () => validator.ValidateAsync(null, null, "foo")); + } + + + [Theory] + [InlineData("")] + [InlineData("abc")] + [InlineData("abcde")] + public async Task FailsIfTooShortTests(string input) + { + const string error = "Passwords must be at least 6 characters."; + var manager = MockHelpers.TestUserManager(); + var valid = new PasswordValidator(); + manager.Options.Password.RequireUppercase = false; + manager.Options.Password.RequireNonAlphanumeric = false; + manager.Options.Password.RequireLowercase = false; + manager.Options.Password.RequireDigit = false; + IdentityResultAssert.IsFailure(await valid.ValidateAsync(manager, null, input), error); + } + + [Theory] + [InlineData("abcdef")] + [InlineData("aaaaaaaaaaa")] + public async Task SuccessIfLongEnoughTests(string input) + { + var manager = MockHelpers.TestUserManager(); + var valid = new PasswordValidator(); + manager.Options.Password.RequireUppercase = false; + manager.Options.Password.RequireNonAlphanumeric = false; + manager.Options.Password.RequireLowercase = false; + manager.Options.Password.RequireDigit = false; + IdentityResultAssert.IsSuccess(await valid.ValidateAsync(manager, null, input)); + } + + [Theory] + [InlineData("a")] + [InlineData("aaaaaaaaaaa")] + public async Task FailsWithoutRequiredNonAlphanumericTests(string input) + { + var manager = MockHelpers.TestUserManager(); + var valid = new PasswordValidator(); + manager.Options.Password.RequireUppercase = false; + manager.Options.Password.RequireNonAlphanumeric = true; + manager.Options.Password.RequireLowercase = false; + manager.Options.Password.RequireDigit = false; + manager.Options.Password.RequiredLength = 0; + IdentityResultAssert.IsFailure(await valid.ValidateAsync(manager, null, input), + "Passwords must have at least one non alphanumeric character."); + } + + [Theory] + [InlineData("@")] + [InlineData("abcd@e!ld!kajfd")] + [InlineData("!!!!!!")] + public async Task SucceedsWithRequiredNonAlphanumericTests(string input) + { + var manager = MockHelpers.TestUserManager(); + var valid = new PasswordValidator(); + manager.Options.Password.RequireUppercase = false; + manager.Options.Password.RequireNonAlphanumeric = true; + manager.Options.Password.RequireLowercase = false; + manager.Options.Password.RequireDigit = false; + manager.Options.Password.RequiredLength = 0; + IdentityResultAssert.IsSuccess(await valid.ValidateAsync(manager, null, input)); + } + + [Theory] + [InlineData("a", 2)] + [InlineData("aaaaaaaaaaa", 2)] + [InlineData("abcdabcdabcdabcdabcdabcdabcd", 5)] + public async Task FailsWithoutRequiredUniqueCharsTests(string input, int uniqueChars) + { + var manager = MockHelpers.TestUserManager(); + var valid = new PasswordValidator(); + manager.Options.Password.RequireUppercase = false; + manager.Options.Password.RequireNonAlphanumeric = false; + manager.Options.Password.RequireLowercase = false; + manager.Options.Password.RequireDigit = false; + manager.Options.Password.RequiredLength = 0; + manager.Options.Password.RequiredUniqueChars = uniqueChars; + IdentityResultAssert.IsFailure(await valid.ValidateAsync(manager, null, input), + String.Format("Passwords must use at least {0} different characters.", uniqueChars)); + } + + [Theory] + [InlineData("12345", 5)] + [InlineData("aAbBc", 5)] + [InlineData("aAbBcaAbBcaAbBc", 5)] + [InlineData("!@#$%", 5)] + [InlineData("a", 1)] + [InlineData("this is a long password with many chars", 10)] + public async Task SucceedsWithRequiredUniqueCharsTests(string input, int uniqueChars) + { + var manager = MockHelpers.TestUserManager(); + var valid = new PasswordValidator(); + manager.Options.Password.RequireUppercase = false; + manager.Options.Password.RequireNonAlphanumeric = false; + manager.Options.Password.RequireLowercase = false; + manager.Options.Password.RequireDigit = false; + manager.Options.Password.RequiredLength = 0; + manager.Options.Password.RequiredUniqueChars = uniqueChars; + IdentityResultAssert.IsSuccess(await valid.ValidateAsync(manager, null, input)); + } + + [Theory] + [InlineData("abcde", Errors.Length | Errors.Alpha | Errors.Upper | Errors.Digit)] + [InlineData("a@B@cd", Errors.Digit)] + [InlineData("___", Errors.Length | Errors.Digit | Errors.Lower | Errors.Upper)] + [InlineData("a_b9de", Errors.Upper)] + [InlineData("abcd@e!ld!kaj9Fd", Errors.None)] + [InlineData("aB1@df", Errors.None)] + public async Task UberMixedRequiredTests(string input, Errors errorMask) + { + const string alphaError = "Passwords must have at least one non alphanumeric character."; + const string upperError = "Passwords must have at least one uppercase ('A'-'Z')."; + const string lowerError = "Passwords must have at least one lowercase ('a'-'z')."; + const string digitError = "Passwords must have at least one digit ('0'-'9')."; + const string lengthError = "Passwords must be at least 6 characters."; + var manager = MockHelpers.TestUserManager(); + var valid = new PasswordValidator(); + var errors = new List(); + if ((errorMask & Errors.Length) != Errors.None) + { + errors.Add(lengthError); + } + if ((errorMask & Errors.Alpha) != Errors.None) + { + errors.Add(alphaError); + } + if ((errorMask & Errors.Digit) != Errors.None) + { + errors.Add(digitError); + } + if ((errorMask & Errors.Lower) != Errors.None) + { + errors.Add(lowerError); + } + if ((errorMask & Errors.Upper) != Errors.None) + { + errors.Add(upperError); + } + var result = await valid.ValidateAsync(manager, null, input); + if (errors.Count == 0) + { + IdentityResultAssert.IsSuccess(result); + } + else + { + IdentityResultAssert.IsFailure(result); + foreach (var error in errors) + { + Assert.Contains(result.Errors, e => e.Description == error); + } + } + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/PrincipalExtensionsTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/PrincipalExtensionsTest.cs new file mode 100644 index 0000000..df9c33e --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/PrincipalExtensionsTest.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class ClaimsIdentityExtensionsTest + { + public const string ExternalAuthenticationScheme = "TestExternalAuth"; + + [Fact] + public void IdentityExtensionsFindFirstValueNullIfUnknownTest() + { + var id = CreateTestExternalIdentity(); + Assert.Null(id.FindFirstValue("bogus")); + } + + private static ClaimsPrincipal CreateTestExternalIdentity() + { + return new ClaimsPrincipal(new ClaimsIdentity( + new[] + { + new Claim(ClaimTypes.NameIdentifier, "NameIdentifier", null, ExternalAuthenticationScheme), + new Claim(ClaimTypes.Name, "Name") + }, + ExternalAuthenticationScheme)); + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/RoleManagerTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/RoleManagerTest.cs new file mode 100644 index 0000000..4e82ec6 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/RoleManagerTest.cs @@ -0,0 +1,212 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class RoleManagerTest + { + [Fact] + public async Task CreateCallsStore() + { + // Setup + var store = new Mock>(); + var role = new TestRole { Name = "Foo" }; + store.Setup(s => s.CreateAsync(role, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + store.Setup(s => s.GetRoleNameAsync(role, CancellationToken.None)).Returns(Task.FromResult(role.Name)).Verifiable(); + store.Setup(s => s.SetNormalizedRoleNameAsync(role, role.Name.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + var roleManager = MockHelpers.TestRoleManager(store.Object); + + // Act + var result = await roleManager.CreateAsync(role); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task UpdateCallsStore() + { + // Setup + var store = new Mock>(); + var role = new TestRole { Name = "Foo" }; + store.Setup(s => s.UpdateAsync(role, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + store.Setup(s => s.GetRoleNameAsync(role, CancellationToken.None)).Returns(Task.FromResult(role.Name)).Verifiable(); + store.Setup(s => s.SetNormalizedRoleNameAsync(role, role.Name.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + var roleManager = MockHelpers.TestRoleManager(store.Object); + + // Act + var result = await roleManager.UpdateAsync(role); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public void RolesQueryableFailWhenStoreNotImplemented() + { + var manager = CreateRoleManager(new NoopRoleStore()); + Assert.False(manager.SupportsQueryableRoles); + Assert.Throws(() => manager.Roles.Count()); + } + + [Fact] + public async Task FindByNameCallsStoreWithNormalizedName() + { + // Setup + var store = new Mock>(); + var role = new TestRole { Name = "Foo" }; + store.Setup(s => s.FindByNameAsync("FOO", CancellationToken.None)).Returns(Task.FromResult(role)).Verifiable(); + var manager = MockHelpers.TestRoleManager(store.Object); + + // Act + var result = await manager.FindByNameAsync(role.Name); + + // Assert + Assert.Equal(role, result); + store.VerifyAll(); + } + + [Fact] + public async Task CanFindByNameCallsStoreWithoutNormalizedName() + { + // Setup + var store = new Mock>(); + var role = new TestRole { Name = "Foo" }; + store.Setup(s => s.FindByNameAsync(role.Name, CancellationToken.None)).Returns(Task.FromResult(role)).Verifiable(); + var manager = MockHelpers.TestRoleManager(store.Object); + manager.KeyNormalizer = null; + + // Act + var result = await manager.FindByNameAsync(role.Name); + + // Assert + Assert.Equal(role, result); + store.VerifyAll(); + } + + [Fact] + public async Task RoleExistsCallsStoreWithNormalizedName() + { + // Setup + var store = new Mock>(); + var role = new TestRole { Name = "Foo" }; + store.Setup(s => s.FindByNameAsync("FOO", CancellationToken.None)).Returns(Task.FromResult(role)).Verifiable(); + var manager = MockHelpers.TestRoleManager(store.Object); + + // Act + var result = await manager.RoleExistsAsync(role.Name); + + // Assert + Assert.True(result); + store.VerifyAll(); + } + + + [Fact] + public void DisposeAfterDisposeDoesNotThrow() + { + var manager = CreateRoleManager(new NoopRoleStore()); + manager.Dispose(); + manager.Dispose(); + } + + [Fact] + public async Task RoleManagerPublicNullChecks() + { + Assert.Throws("store", + () => new RoleManager(null, null, null, null, null)); + var manager = CreateRoleManager(new NotImplementedStore()); + await Assert.ThrowsAsync("role", async () => await manager.CreateAsync(null)); + await Assert.ThrowsAsync("role", async () => await manager.UpdateAsync(null)); + await Assert.ThrowsAsync("role", async () => await manager.DeleteAsync(null)); + await Assert.ThrowsAsync("roleName", async () => await manager.FindByNameAsync(null)); + await Assert.ThrowsAsync("roleName", async () => await manager.RoleExistsAsync(null)); + } + + [Fact] + public async Task RoleStoreMethodsThrowWhenDisposed() + { + var manager = CreateRoleManager(new NoopRoleStore()); + manager.Dispose(); + await Assert.ThrowsAsync(() => manager.FindByIdAsync(null)); + await Assert.ThrowsAsync(() => manager.FindByNameAsync(null)); + await Assert.ThrowsAsync(() => manager.RoleExistsAsync(null)); + await Assert.ThrowsAsync(() => manager.CreateAsync(null)); + await Assert.ThrowsAsync(() => manager.UpdateAsync(null)); + await Assert.ThrowsAsync(() => manager.DeleteAsync(null)); + } + + private static RoleManager CreateRoleManager(IRoleStore roleStore) + { + return MockHelpers.TestRoleManager(roleStore); + } + + private class NotImplementedStore : IRoleStore + { + public Task CreateAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task UpdateAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetRoleIdAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetRoleNameAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetRoleNameAsync(TestRole role, string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task FindByIdAsync(string roleId, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task FindByNameAsync(string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + throw new NotImplementedException(); + } + + public Task GetNormalizedRoleNameAsync(TestRole role, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedRoleNameAsync(TestRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/RoleValidatorTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/RoleValidatorTest.cs new file mode 100644 index 0000000..9f6040c --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/RoleValidatorTest.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class RoleValidatorTest + { + [Fact] + public async Task ValidateThrowsWithNull() + { + // Setup + var validator = new RoleValidator(); + var manager = MockHelpers.TestRoleManager(); + + // Act + // Assert + await Assert.ThrowsAsync("manager", async () => await validator.ValidateAsync(null, null)); + await Assert.ThrowsAsync("role", async () => await validator.ValidateAsync(manager, null)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public async Task ValidateFailsWithTooShortRoleName(string input) + { + // Setup + var validator = new RoleValidator(); + var manager = MockHelpers.TestRoleManager(); + var user = new TestRole {Name = input}; + + // Act + var result = await validator.ValidateAsync(manager, user); + + // Assert + IdentityResultAssert.IsFailure(result, new IdentityErrorDescriber().InvalidRoleName(input)); + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/SecurityStampValidatorTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/SecurityStampValidatorTest.cs new file mode 100644 index 0000000..cbbd961 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/SecurityStampValidatorTest.cs @@ -0,0 +1,220 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class SecurityStampTest + { + private class NoopHandler : IAuthenticationHandler + { + public Task AuthenticateAsync() + { + throw new NotImplementedException(); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task HandleRequestAsync() + { + throw new NotImplementedException(); + } + + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + throw new NotImplementedException(); + } + + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + + public Task SignOutAsync(AuthenticationProperties properties) + { + throw new NotImplementedException(); + } + } + + [Fact] + public async Task OnValidatePrincipalThrowsWithEmptyServiceCollection() + { + var httpContext = new Mock(); + httpContext.Setup(c => c.RequestServices).Returns(new ServiceCollection().BuildServiceProvider()); + var id = new ClaimsPrincipal(new ClaimsIdentity(IdentityConstants.ApplicationScheme)); + var ticket = new AuthenticationTicket(id, new AuthenticationProperties { IssuedUtc = DateTimeOffset.UtcNow }, IdentityConstants.ApplicationScheme); + var context = new CookieValidatePrincipalContext(httpContext.Object, new AuthenticationSchemeBuilder(IdentityConstants.ApplicationScheme) { HandlerType = typeof(NoopHandler) }.Build(), new CookieAuthenticationOptions(), ticket); + var ex = await Assert.ThrowsAsync(() => SecurityStampValidator.ValidatePrincipalAsync(context)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task OnValidatePrincipalTestSuccess(bool isPersistent) + { + var user = new TestUser("test"); + var userManager = MockHelpers.MockUserManager(); + var claimsManager = new Mock>(); + var identityOptions = new Mock>(); + identityOptions.Setup(a => a.Value).Returns(new IdentityOptions()); + var options = new Mock>(); + options.Setup(a => a.Value).Returns(new SecurityStampValidatorOptions { ValidationInterval = TimeSpan.Zero }); + var httpContext = new Mock(); + var contextAccessor = new Mock(); + contextAccessor.Setup(a => a.HttpContext).Returns(httpContext.Object); + var id = new ClaimsIdentity(IdentityConstants.ApplicationScheme); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id)); + var principal = new ClaimsPrincipal(id); + + var properties = new AuthenticationProperties { IssuedUtc = DateTimeOffset.UtcNow.AddSeconds(-1), IsPersistent = isPersistent }; + var signInManager = new Mock>(userManager.Object, + contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock().Object); + signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny())).ReturnsAsync(user).Verifiable(); + signInManager.Setup(s => s.CreateUserPrincipalAsync(user)).ReturnsAsync(principal).Verifiable(); + var services = new ServiceCollection(); + services.AddSingleton(options.Object); + services.AddSingleton(signInManager.Object); + services.AddSingleton(new SecurityStampValidator(options.Object, signInManager.Object, new SystemClock())); + httpContext.Setup(c => c.RequestServices).Returns(services.BuildServiceProvider()); + + var ticket = new AuthenticationTicket(principal, + properties, + IdentityConstants.ApplicationScheme); + var context = new CookieValidatePrincipalContext(httpContext.Object, new AuthenticationSchemeBuilder(IdentityConstants.ApplicationScheme) { HandlerType = typeof(NoopHandler) }.Build(), new CookieAuthenticationOptions(), ticket); + Assert.NotNull(context.Properties); + Assert.NotNull(context.Options); + Assert.NotNull(context.Principal); + await + SecurityStampValidator.ValidatePrincipalAsync(context); + Assert.NotNull(context.Principal); + signInManager.VerifyAll(); + } + + [Fact] + public async Task OnValidateIdentityRejectsWhenValidateSecurityStampFails() + { + var user = new TestUser("test"); + var userManager = MockHelpers.MockUserManager(); + var claimsManager = new Mock>(); + var identityOptions = new Mock>(); + identityOptions.Setup(a => a.Value).Returns(new IdentityOptions()); + var options = new Mock>(); + options.Setup(a => a.Value).Returns(new SecurityStampValidatorOptions { ValidationInterval = TimeSpan.Zero }); + var httpContext = new Mock(); + var contextAccessor = new Mock(); + contextAccessor.Setup(a => a.HttpContext).Returns(httpContext.Object); + var signInManager = new Mock>(userManager.Object, + contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock().Object); + signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny())).ReturnsAsync(default(TestUser)).Verifiable(); + var services = new ServiceCollection(); + services.AddSingleton(options.Object); + services.AddSingleton(signInManager.Object); + services.AddSingleton(new SecurityStampValidator(options.Object, signInManager.Object, new SystemClock())); + httpContext.Setup(c => c.RequestServices).Returns(services.BuildServiceProvider()); + var id = new ClaimsIdentity(IdentityConstants.ApplicationScheme); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id)); + + var ticket = new AuthenticationTicket(new ClaimsPrincipal(id), + new AuthenticationProperties { IssuedUtc = DateTimeOffset.UtcNow.AddSeconds(-1) }, + IdentityConstants.ApplicationScheme); + var context = new CookieValidatePrincipalContext(httpContext.Object, new AuthenticationSchemeBuilder(IdentityConstants.ApplicationScheme) { HandlerType = typeof(NoopHandler) }.Build(), new CookieAuthenticationOptions(), ticket); + Assert.NotNull(context.Properties); + Assert.NotNull(context.Options); + Assert.NotNull(context.Principal); + await SecurityStampValidator.ValidatePrincipalAsync(context); + Assert.Null(context.Principal); + signInManager.VerifyAll(); + } + + [Fact] + public async Task OnValidateIdentityRejectsWhenNoIssuedUtc() + { + var user = new TestUser("test"); + var httpContext = new Mock(); + var userManager = MockHelpers.MockUserManager(); + var identityOptions = new Mock>(); + identityOptions.Setup(a => a.Value).Returns(new IdentityOptions()); + var claimsManager = new Mock>(); + var options = new Mock>(); + options.Setup(a => a.Value).Returns(new SecurityStampValidatorOptions { ValidationInterval = TimeSpan.Zero }); + var contextAccessor = new Mock(); + contextAccessor.Setup(a => a.HttpContext).Returns(httpContext.Object); + var signInManager = new Mock>(userManager.Object, + contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock().Object); + signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny())).ReturnsAsync(default(TestUser)).Verifiable(); + var services = new ServiceCollection(); + services.AddSingleton(options.Object); + services.AddSingleton(signInManager.Object); + services.AddSingleton(new SecurityStampValidator(options.Object, signInManager.Object, new SystemClock())); + httpContext.Setup(c => c.RequestServices).Returns(services.BuildServiceProvider()); + var id = new ClaimsIdentity(IdentityConstants.ApplicationScheme); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id)); + + var ticket = new AuthenticationTicket(new ClaimsPrincipal(id), + new AuthenticationProperties(), + IdentityConstants.ApplicationScheme); + var context = new CookieValidatePrincipalContext(httpContext.Object, new AuthenticationSchemeBuilder(IdentityConstants.ApplicationScheme) { HandlerType = typeof(NoopHandler) }.Build(), new CookieAuthenticationOptions(), ticket); + Assert.NotNull(context.Properties); + Assert.NotNull(context.Options); + Assert.NotNull(context.Principal); + await SecurityStampValidator.ValidatePrincipalAsync(context); + Assert.Null(context.Principal); + signInManager.VerifyAll(); + } + + [Fact] + public async Task OnValidateIdentityDoesNotRejectsWhenNotExpired() + { + var user = new TestUser("test"); + var httpContext = new Mock(); + var userManager = MockHelpers.MockUserManager(); + var identityOptions = new Mock>(); + identityOptions.Setup(a => a.Value).Returns(new IdentityOptions()); + var claimsManager = new Mock>(); + var options = new Mock>(); + options.Setup(a => a.Value).Returns(new SecurityStampValidatorOptions { ValidationInterval = TimeSpan.FromDays(1) }); + var contextAccessor = new Mock(); + contextAccessor.Setup(a => a.HttpContext).Returns(httpContext.Object); + var signInManager = new Mock>(userManager.Object, + contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock().Object); + signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny())).Throws(new Exception("Shouldn't be called")); + signInManager.Setup(s => s.SignInAsync(user, false, null)).Throws(new Exception("Shouldn't be called")); + var services = new ServiceCollection(); + services.AddSingleton(options.Object); + services.AddSingleton(signInManager.Object); + services.AddSingleton(new SecurityStampValidator(options.Object, signInManager.Object, new SystemClock())); + httpContext.Setup(c => c.RequestServices).Returns(services.BuildServiceProvider()); + var id = new ClaimsIdentity(IdentityConstants.ApplicationScheme); + id.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id)); + + var ticket = new AuthenticationTicket(new ClaimsPrincipal(id), + new AuthenticationProperties { IssuedUtc = DateTimeOffset.UtcNow }, + IdentityConstants.ApplicationScheme); + var context = new CookieValidatePrincipalContext(httpContext.Object, new AuthenticationSchemeBuilder(IdentityConstants.ApplicationScheme) { HandlerType = typeof(NoopHandler) }.Build(), new CookieAuthenticationOptions(), ticket); + Assert.NotNull(context.Properties); + Assert.NotNull(context.Options); + Assert.NotNull(context.Principal); + await SecurityStampValidator.ValidatePrincipalAsync(context); + Assert.NotNull(context.Principal); + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/SignInManagerTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/SignInManagerTest.cs new file mode 100644 index 0000000..ff746ad --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/SignInManagerTest.cs @@ -0,0 +1,866 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class SignManagerInTest + { + //[Theory] + //[InlineData(true)] + //[InlineData(false)] + //public async Task VerifyAccountControllerSignInFunctional(bool isPersistent) + //{ + // var app = new ApplicationBuilder(new ServiceCollection().BuildServiceProvider()); + // app.UseCookieAuthentication(new CookieAuthenticationOptions + // { + // AuthenticationScheme = ClaimsIdentityOptions.DefaultAuthenticationScheme + // }); + + // TODO: how to functionally test context? + // var context = new DefaultHttpContext(new FeatureCollection()); + // var contextAccessor = new Mock(); + // contextAccessor.Setup(a => a.Value).Returns(context); + // app.UseServices(services => + // { + // services.AddSingleton(contextAccessor.Object); + // services.AddSingleton(new Nulllogger()); + // services.AddIdentity(s => + // { + // s.AddUserStore(() => new InMemoryUserStore()); + // s.AddUserManager(); + // s.AddRoleStore(() => new InMemoryRoleStore()); + // s.AddRoleManager(); + // }); + // services.AddTransient(); + // }); + + // // Act + // var user = new ApplicationUser + // { + // UserName = "Yolo" + // }; + // const string password = "Yol0Sw@g!"; + // var userManager = app.ApplicationServices.GetRequiredService(); + // var HttpSignInManager = app.ApplicationServices.GetRequiredService(); + + // IdentityResultAssert.IsSuccess(await userManager.CreateAsync(user, password)); + // var result = await HttpSignInManager.PasswordSignInAsync(user.UserName, password, isPersistent, false); + + // // Assert + // Assert.Equal(SignInStatus.Success, result); + // contextAccessor.Verify(); + //} + + [Fact] + public void ConstructorNullChecks() + { + Assert.Throws("userManager", () => new SignInManager(null, null, null, null, null, null)); + var userManager = MockHelpers.MockUserManager().Object; + Assert.Throws("contextAccessor", () => new SignInManager(userManager, null, null, null, null, null)); + var contextAccessor = new Mock(); + var context = new Mock(); + contextAccessor.Setup(a => a.HttpContext).Returns(context.Object); + Assert.Throws("claimsFactory", () => new SignInManager(userManager, contextAccessor.Object, null, null, null, null)); + } + + //[Fact] + //public async Task EnsureClaimsPrincipalFactoryCreateIdentityCalled() + //{ + // // Setup + // var user = new TestUser { UserName = "Foo" }; + // var userManager = MockHelpers.TestUserManager(); + // var identityFactory = new Mock>(); + // const string authType = "Test"; + // var testIdentity = new ClaimsPrincipal(); + // identityFactory.Setup(s => s.CreateAsync(user)).ReturnsAsync(testIdentity).Verifiable(); + // var context = new Mock(); + // var response = new Mock(); + // context.Setup(c => c.Response).Returns(response.Object).Verifiable(); + // response.Setup(r => r.SignIn(testIdentity, It.IsAny())).Verifiable(); + // var contextAccessor = new Mock(); + // contextAccessor.Setup(a => a.Value).Returns(context.Object); + // var helper = new HttpAuthenticationManager(contextAccessor.Object); + + // // Act + // helper.SignIn(user, false); + + // // Assert + // identityFactory.Verify(); + // context.Verify(); + // contextAccessor.Verify(); + // response.Verify(); + //} + + [Fact] + public async Task PasswordSignInReturnsLockedOutWhenLockedOut() + { + // Setup + var user = new TestUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); + manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(true).Verifiable(); + + var context = new Mock(); + var contextAccessor = new Mock(); + contextAccessor.Setup(a => a.HttpContext).Returns(context.Object); + var roleManager = MockHelpers.MockRoleManager(); + var identityOptions = new IdentityOptions(); + var options = new Mock>(); + options.Setup(a => a.Value).Returns(identityOptions); + var claimsFactory = new UserClaimsPrincipalFactory(manager.Object, roleManager.Object, options.Object); + var logStore = new StringBuilder(); + var logger = MockHelpers.MockILogger>(logStore); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory, options.Object, logger.Object, new Mock().Object); + + // Act + var result = await helper.PasswordSignInAsync(user.UserName, "bogus", false, false); + + // Assert + Assert.False(result.Succeeded); + Assert.True(result.IsLockedOut); + Assert.Contains($"User {user.Id} is currently locked out.", logStore.ToString()); + manager.Verify(); + } + + [Fact] + public async Task CheckPasswordSignInReturnsLockedOutWhenLockedOut() + { + // Setup + var user = new TestUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); + manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(true).Verifiable(); + + var context = new Mock(); + var contextAccessor = new Mock(); + contextAccessor.Setup(a => a.HttpContext).Returns(context.Object); + var roleManager = MockHelpers.MockRoleManager(); + var identityOptions = new IdentityOptions(); + var options = new Mock>(); + options.Setup(a => a.Value).Returns(identityOptions); + var claimsFactory = new UserClaimsPrincipalFactory(manager.Object, roleManager.Object, options.Object); + var logStore = new StringBuilder(); + var logger = MockHelpers.MockILogger>(logStore); + var helper = new SignInManager(manager.Object, contextAccessor.Object, claimsFactory, options.Object, logger.Object, new Mock().Object); + + // Act + var result = await helper.CheckPasswordSignInAsync(user, "bogus", false); + + // Assert + Assert.False(result.Succeeded); + Assert.True(result.IsLockedOut); + Assert.Contains($"User {user.Id} is currently locked out.", logStore.ToString()); + manager.Verify(); + } + + private static Mock> SetupUserManager(TestUser user) + { + var manager = MockHelpers.MockUserManager(); + manager.Setup(m => m.FindByNameAsync(user.UserName)).ReturnsAsync(user); + manager.Setup(m => m.FindByIdAsync(user.Id)).ReturnsAsync(user); + manager.Setup(m => m.GetUserIdAsync(user)).ReturnsAsync(user.Id.ToString()); + manager.Setup(m => m.GetUserNameAsync(user)).ReturnsAsync(user.UserName); + return manager; + } + + private static SignInManager SetupSignInManager(UserManager manager, HttpContext context, StringBuilder logStore = null, IdentityOptions identityOptions = null) + { + var contextAccessor = new Mock(); + contextAccessor.Setup(a => a.HttpContext).Returns(context); + var roleManager = MockHelpers.MockRoleManager(); + identityOptions = identityOptions ?? new IdentityOptions(); + var options = new Mock>(); + options.Setup(a => a.Value).Returns(identityOptions); + var claimsFactory = new UserClaimsPrincipalFactory(manager, roleManager.Object, options.Object); + var sm = new SignInManager(manager, contextAccessor.Object, claimsFactory, options.Object, null, new Mock().Object); + sm.Logger = MockHelpers.MockILogger>(logStore ?? new StringBuilder()).Object; + return sm; + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CanPasswordSignIn(bool isPersistent) + { + // Setup + var user = new TestUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); + manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); + manager.Setup(m => m.CheckPasswordAsync(user, "password")).ReturnsAsync(true).Verifiable(); + + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + SetupSignIn(context, auth, user.Id, isPersistent); + var helper = SetupSignInManager(manager.Object, context); + + // Act + var result = await helper.PasswordSignInAsync(user.UserName, "password", isPersistent, false); + + // Assert + Assert.True(result.Succeeded); + manager.Verify(); + auth.Verify(); + } + + [Fact] + public async Task CanPasswordSignInWithNoLogger() + { + // Setup + var user = new TestUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); + manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); + manager.Setup(m => m.CheckPasswordAsync(user, "password")).ReturnsAsync(true).Verifiable(); + + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + SetupSignIn(context, auth, user.Id, false); + var helper = SetupSignInManager(manager.Object, context); + + // Act + var result = await helper.PasswordSignInAsync(user.UserName, "password", false, false); + + // Assert + Assert.True(result.Succeeded); + manager.Verify(); + auth.Verify(); + } + + + [Fact] + public async Task PasswordSignInWorksWithNonTwoFactorStore() + { + // Setup + var user = new TestUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); + manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); + manager.Setup(m => m.CheckPasswordAsync(user, "password")).ReturnsAsync(true).Verifiable(); + manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); + + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + SetupSignIn(context, auth); + var helper = SetupSignInManager(manager.Object, context); + + // Act + var result = await helper.PasswordSignInAsync(user.UserName, "password", false, false); + + // Assert + Assert.True(result.Succeeded); + manager.Verify(); + auth.Verify(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task PasswordSignInRequiresVerification(bool supportsLockout) + { + // Setup + var user = new TestUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + manager.Setup(m => m.SupportsUserLockout).Returns(supportsLockout).Verifiable(); + if (supportsLockout) + { + manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); + } + IList providers = new List(); + providers.Add("PhoneNumber"); + manager.Setup(m => m.GetValidTwoFactorProvidersAsync(user)).Returns(Task.FromResult(providers)).Verifiable(); + manager.Setup(m => m.SupportsUserTwoFactor).Returns(true).Verifiable(); + manager.Setup(m => m.GetTwoFactorEnabledAsync(user)).ReturnsAsync(true).Verifiable(); + manager.Setup(m => m.CheckPasswordAsync(user, "password")).ReturnsAsync(true).Verifiable(); + if (supportsLockout) + { + manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); + } + var context = new DefaultHttpContext(); + var helper = SetupSignInManager(manager.Object, context); + var auth = MockAuth(context); + auth.Setup(a => a.SignInAsync(context, IdentityConstants.TwoFactorUserIdScheme, + It.Is(id => id.FindFirstValue(ClaimTypes.Name) == user.Id), + It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + + // Act + var result = await helper.PasswordSignInAsync(user.UserName, "password", false, false); + + // Assert + Assert.False(result.Succeeded); + Assert.True(result.RequiresTwoFactor); + manager.Verify(); + auth.Verify(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ExternalSignInRequiresVerificationIfNotBypassed(bool bypass) + { + // Setup + var user = new TestUser { UserName = "Foo" }; + const string loginProvider = "login"; + const string providerKey = "fookey"; + var manager = SetupUserManager(user); + manager.Setup(m => m.SupportsUserLockout).Returns(false).Verifiable(); + manager.Setup(m => m.FindByLoginAsync(loginProvider, providerKey)).ReturnsAsync(user).Verifiable(); + if (!bypass) + { + IList providers = new List(); + providers.Add("PhoneNumber"); + manager.Setup(m => m.GetValidTwoFactorProvidersAsync(user)).Returns(Task.FromResult(providers)).Verifiable(); + manager.Setup(m => m.SupportsUserTwoFactor).Returns(true).Verifiable(); + manager.Setup(m => m.GetTwoFactorEnabledAsync(user)).ReturnsAsync(true).Verifiable(); + } + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + var helper = SetupSignInManager(manager.Object, context); + + if (bypass) + { + SetupSignIn(context, auth, user.Id, false, loginProvider); + } + else + { + auth.Setup(a => a.SignInAsync(context, IdentityConstants.TwoFactorUserIdScheme, + It.Is(id => id.FindFirstValue(ClaimTypes.Name) == user.Id), + It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + } + + // Act + var result = await helper.ExternalLoginSignInAsync(loginProvider, providerKey, isPersistent: false, bypassTwoFactor: bypass); + + // Assert + Assert.Equal(bypass, result.Succeeded); + Assert.Equal(!bypass, result.RequiresTwoFactor); + manager.Verify(); + auth.Verify(); + } + + private class GoodTokenProvider : AuthenticatorTokenProvider + { + public override Task ValidateAsync(string purpose, string token, UserManager manager, TestUser user) + { + return Task.FromResult(true); + } + } + + + //[Theory] + //[InlineData(null, true, true)] + //[InlineData("Authenticator", false, true)] + //[InlineData("Gooblygook", true, false)] + //[InlineData("--", false, false)] + //public async Task CanTwoFactorAuthenticatorSignIn(string providerName, bool isPersistent, bool rememberClient) + //{ + // // Setup + // var user = new TestUser { UserName = "Foo" }; + // const string code = "3123"; + // var manager = SetupUserManager(user); + // manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); + // manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, providerName ?? TokenOptions.DefaultAuthenticatorProvider, code)).ReturnsAsync(true).Verifiable(); + // manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); + + // var context = new DefaultHttpContext(); + // var auth = MockAuth(context); + // var helper = SetupSignInManager(manager.Object, context); + // var twoFactorInfo = new SignInManager.TwoFactorAuthenticationInfo { UserId = user.Id }; + // if (providerName != null) + // { + // helper.Options.Tokens.AuthenticatorTokenProvider = providerName; + // } + // var id = helper.StoreTwoFactorInfo(user.Id, null); + // SetupSignIn(context, auth, user.Id, isPersistent); + // auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorUserIdScheme)) + // .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(id, null, IdentityConstants.TwoFactorUserIdScheme))).Verifiable(); + // if (rememberClient) + // { + // auth.Setup(a => a.SignInAsync(context, + // IdentityConstants.TwoFactorRememberMeScheme, + // It.Is(i => i.FindFirstValue(ClaimTypes.Name) == user.Id + // && i.Identities.First().AuthenticationType == IdentityConstants.TwoFactorRememberMeScheme), + // It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + // } + + // // Act + // var result = await helper.TwoFactorAuthenticatorSignInAsync(code, isPersistent, rememberClient); + + // // Assert + // Assert.True(result.Succeeded); + // manager.Verify(); + // auth.Verify(); + //} + + //[Theory] + //[InlineData(true, true)] + //[InlineData(true, false)] + //[InlineData(false, true)] + //[InlineData(false, false)] + //public async Task CanTwoFactorRecoveryCodeSignIn(bool supportsLockout, bool externalLogin) + //{ + // // Setup + // var user = new TestUser { UserName = "Foo" }; + // const string bypassCode = "someCode"; + // var manager = SetupUserManager(user); + // manager.Setup(m => m.SupportsUserLockout).Returns(supportsLockout).Verifiable(); + // manager.Setup(m => m.RedeemTwoFactorRecoveryCodeAsync(user, bypassCode)).ReturnsAsync(IdentityResult.Success).Verifiable(); + // if (supportsLockout) + // { + // manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); + // } + // var context = new DefaultHttpContext(); + // var auth = MockAuth(context); + // var helper = SetupSignInManager(manager.Object, context); + // var twoFactorInfo = new SignInManager.TwoFactorAuthenticationInfo { UserId = user.Id }; + // var loginProvider = "loginprovider"; + // var id = helper.StoreTwoFactorInfo(user.Id, externalLogin ? loginProvider : null); + // if (externalLogin) + // { + // auth.Setup(a => a.SignInAsync(context, + // IdentityConstants.ApplicationScheme, + // It.Is(i => i.FindFirstValue(ClaimTypes.AuthenticationMethod) == loginProvider + // && i.FindFirstValue(ClaimTypes.NameIdentifier) == user.Id), + // It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + // auth.Setup(a => a.SignOutAsync(context, IdentityConstants.ExternalScheme, It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + // auth.Setup(a => a.SignOutAsync(context, IdentityConstants.TwoFactorUserIdScheme, It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + // } + // else + // { + // SetupSignIn(context, auth, user.Id); + // } + // auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorUserIdScheme)) + // .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(id, null, IdentityConstants.TwoFactorUserIdScheme))).Verifiable(); + + // // Act + // var result = await helper.TwoFactorRecoveryCodeSignInAsync(bypassCode); + + // // Assert + // Assert.True(result.Succeeded); + // manager.Verify(); + // auth.Verify(); + //} + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public async Task CanExternalSignIn(bool isPersistent, bool supportsLockout) + { + // Setup + var user = new TestUser { UserName = "Foo" }; + const string loginProvider = "login"; + const string providerKey = "fookey"; + var manager = SetupUserManager(user); + manager.Setup(m => m.SupportsUserLockout).Returns(supportsLockout).Verifiable(); + if (supportsLockout) + { + manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); + } + manager.Setup(m => m.FindByLoginAsync(loginProvider, providerKey)).ReturnsAsync(user).Verifiable(); + + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + var helper = SetupSignInManager(manager.Object, context); + SetupSignIn(context, auth, user.Id, isPersistent, loginProvider); + + // Act + var result = await helper.ExternalLoginSignInAsync(loginProvider, providerKey, isPersistent); + + // Assert + Assert.True(result.Succeeded); + manager.Verify(); + auth.Verify(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CanResignIn(bool externalLogin) + { + // Setup + var user = new TestUser { UserName = "Foo" }; + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + var loginProvider = "loginprovider"; + var id = new ClaimsIdentity(); + if (externalLogin) + { + id.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, loginProvider)); + } + // REVIEW: auth changes we lost the ability to mock is persistent + //var properties = new AuthenticationProperties { IsPersistent = isPersistent }; + var authResult = AuthenticateResult.NoResult(); + auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.ApplicationScheme)) + .Returns(Task.FromResult(authResult)).Verifiable(); + var manager = SetupUserManager(user); + var signInManager = new Mock>(manager.Object, + new HttpContextAccessor { HttpContext = context }, + new Mock>().Object, + null, null, new Mock().Object) + { CallBase = true }; + //signInManager.Setup(s => s.SignInAsync(user, It.Is(p => p.IsPersistent == isPersistent), + //externalLogin? loginProvider : null)).Returns(Task.FromResult(0)).Verifiable(); + signInManager.Setup(s => s.SignInAsync(user, It.IsAny(), null)).Returns(Task.FromResult(0)).Verifiable(); + signInManager.Object.Context = context; + + // Act + await signInManager.Object.RefreshSignInAsync(user); + + // Assert + auth.Verify(); + signInManager.Verify(); + } + + //[Theory] + //[InlineData(true, true, true, true)] + //[InlineData(true, true, false, true)] + //[InlineData(true, false, true, true)] + //[InlineData(true, false, false, true)] + //[InlineData(false, true, true, true)] + //[InlineData(false, true, false, true)] + //[InlineData(false, false, true, true)] + //[InlineData(false, false, false, true)] + //[InlineData(true, true, true, false)] + //[InlineData(true, true, false, false)] + //[InlineData(true, false, true, false)] + //[InlineData(true, false, false, false)] + //[InlineData(false, true, true, false)] + //[InlineData(false, true, false, false)] + //[InlineData(false, false, true, false)] + //[InlineData(false, false, false, false)] + //public async Task CanTwoFactorSignIn(bool isPersistent, bool supportsLockout, bool externalLogin, bool rememberClient) + //{ + // // Setup + // var user = new TestUser { UserName = "Foo" }; + // var manager = SetupUserManager(user); + // var provider = "twofactorprovider"; + // var code = "123456"; + // manager.Setup(m => m.SupportsUserLockout).Returns(supportsLockout).Verifiable(); + // if (supportsLockout) + // { + // manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); + // manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable(); + // } + // manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, provider, code)).ReturnsAsync(true).Verifiable(); + // var context = new DefaultHttpContext(); + // var auth = MockAuth(context); + // var helper = SetupSignInManager(manager.Object, context); + // var twoFactorInfo = new SignInManager.TwoFactorAuthenticationInfo { UserId = user.Id }; + // var loginProvider = "loginprovider"; + // var id = helper.StoreTwoFactorInfo(user.Id, externalLogin ? loginProvider : null); + // if (externalLogin) + // { + // auth.Setup(a => a.SignInAsync(context, + // IdentityConstants.ApplicationScheme, + // It.Is(i => i.FindFirstValue(ClaimTypes.AuthenticationMethod) == loginProvider + // && i.FindFirstValue(ClaimTypes.NameIdentifier) == user.Id), + // It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + // // REVIEW: restore ability to test is persistent + // //It.Is(v => v.IsPersistent == isPersistent))).Verifiable(); + // auth.Setup(a => a.SignOutAsync(context, IdentityConstants.ExternalScheme, It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + // auth.Setup(a => a.SignOutAsync(context, IdentityConstants.TwoFactorUserIdScheme, It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + // } + // else + // { + // SetupSignIn(context, auth, user.Id); + // } + // if (rememberClient) + // { + // auth.Setup(a => a.SignInAsync(context, + // IdentityConstants.TwoFactorRememberMeScheme, + // It.Is(i => i.FindFirstValue(ClaimTypes.Name) == user.Id + // && i.Identities.First().AuthenticationType == IdentityConstants.TwoFactorRememberMeScheme), + // It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + // //It.Is(v => v.IsPersistent == true))).Returns(Task.FromResult(0)).Verifiable(); + // } + // auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorUserIdScheme)) + // .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(id, null, IdentityConstants.TwoFactorUserIdScheme))).Verifiable(); + + // // Act + // var result = await helper.TwoFactorSignInAsync(provider, code, isPersistent, rememberClient); + + // // Assert + // Assert.True(result.Succeeded); + // manager.Verify(); + // auth.Verify(); + //} + + [Fact] + public async Task RememberClientStoresUserId() + { + // Setup + var user = new TestUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + var helper = SetupSignInManager(manager.Object, context); + auth.Setup(a => a.SignInAsync( + context, + IdentityConstants.TwoFactorRememberMeScheme, + It.Is(i => i.FindFirstValue(ClaimTypes.Name) == user.Id + && i.Identities.First().AuthenticationType == IdentityConstants.TwoFactorRememberMeScheme), + It.Is(v => v.IsPersistent == true))).Returns(Task.FromResult(0)).Verifiable(); + + + // Act + await helper.RememberTwoFactorClientAsync(user); + + // Assert + manager.Verify(); + auth.Verify(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task RememberBrowserSkipsTwoFactorVerificationSignIn(bool isPersistent) + { + // Setup + var user = new TestUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + manager.Setup(m => m.GetTwoFactorEnabledAsync(user)).ReturnsAsync(true).Verifiable(); + IList providers = new List(); + providers.Add("PhoneNumber"); + manager.Setup(m => m.GetValidTwoFactorProvidersAsync(user)).Returns(Task.FromResult(providers)).Verifiable(); + manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); + manager.Setup(m => m.SupportsUserTwoFactor).Returns(true).Verifiable(); + manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); + manager.Setup(m => m.CheckPasswordAsync(user, "password")).ReturnsAsync(true).Verifiable(); + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + SetupSignIn(context, auth); + var id = new ClaimsIdentity(IdentityConstants.TwoFactorRememberMeScheme); + id.AddClaim(new Claim(ClaimTypes.Name, user.Id)); + auth.Setup(a => a.AuthenticateAsync(context, IdentityConstants.TwoFactorRememberMeScheme)) + .ReturnsAsync(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(id), null, IdentityConstants.TwoFactorRememberMeScheme))).Verifiable(); + var helper = SetupSignInManager(manager.Object, context); + + // Act + var result = await helper.PasswordSignInAsync(user.UserName, "password", isPersistent, false); + + // Assert + Assert.True(result.Succeeded); + manager.Verify(); + auth.Verify(); + } + + private Mock MockAuth(HttpContext context) + { + var auth = new Mock(); + context.RequestServices = new ServiceCollection().AddSingleton(auth.Object).BuildServiceProvider(); + return auth; + } + + [Fact] + public async Task SignOutCallsContextResponseSignOut() + { + // Setup + var manager = MockHelpers.TestUserManager(); + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + auth.Setup(a => a.SignOutAsync(context, IdentityConstants.ApplicationScheme, It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + auth.Setup(a => a.SignOutAsync(context, IdentityConstants.TwoFactorUserIdScheme, It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + auth.Setup(a => a.SignOutAsync(context, IdentityConstants.ExternalScheme, It.IsAny())).Returns(Task.FromResult(0)).Verifiable(); + var helper = SetupSignInManager(manager, context, null, manager.Options); + + // Act + await helper.SignOutAsync(); + + // Assert + auth.Verify(); + } + + [Fact] + public async Task PasswordSignInFailsWithWrongPassword() + { + // Setup + var user = new TestUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); + manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable(); + manager.Setup(m => m.CheckPasswordAsync(user, "bogus")).ReturnsAsync(false).Verifiable(); + var context = new Mock(); + var logStore = new StringBuilder(); + var helper = SetupSignInManager(manager.Object, context.Object, logStore); + // Act + var result = await helper.PasswordSignInAsync(user.UserName, "bogus", false, false); + var checkResult = await helper.CheckPasswordSignInAsync(user, "bogus", false); + + // Assert + Assert.False(result.Succeeded); + Assert.False(checkResult.Succeeded); + Assert.True(logStore.ToString().Contains($"User {user.Id} failed to provide the correct password.")); + manager.Verify(); + context.Verify(); + } + + [Fact] + public async Task PasswordSignInFailsWithUnknownUser() + { + // Setup + var manager = MockHelpers.MockUserManager(); + manager.Setup(m => m.FindByNameAsync("bogus")).ReturnsAsync(default(TestUser)).Verifiable(); + var context = new Mock(); + var helper = SetupSignInManager(manager.Object, context.Object); + + // Act + var result = await helper.PasswordSignInAsync("bogus", "bogus", false, false); + + // Assert + Assert.False(result.Succeeded); + manager.Verify(); + context.Verify(); + } + + [Fact] + public async Task PasswordSignInFailsWithWrongPasswordCanAccessFailedAndLockout() + { + // Setup + var user = new TestUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + var lockedout = false; + manager.Setup(m => m.AccessFailedAsync(user)).Returns(() => + { + lockedout = true; + return Task.FromResult(IdentityResult.Success); + }).Verifiable(); + manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); + manager.Setup(m => m.IsLockedOutAsync(user)).Returns(() => Task.FromResult(lockedout)); + manager.Setup(m => m.CheckPasswordAsync(user, "bogus")).ReturnsAsync(false).Verifiable(); + var context = new Mock(); + var helper = SetupSignInManager(manager.Object, context.Object); + + // Act + var result = await helper.PasswordSignInAsync(user.UserName, "bogus", false, true); + + // Assert + Assert.False(result.Succeeded); + Assert.True(result.IsLockedOut); + manager.Verify(); + } + + [Fact] + public async Task CheckPasswordSignInFailsWithWrongPasswordCanAccessFailedAndLockout() + { + // Setup + var user = new TestUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + var lockedout = false; + manager.Setup(m => m.AccessFailedAsync(user)).Returns(() => + { + lockedout = true; + return Task.FromResult(IdentityResult.Success); + }).Verifiable(); + manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable(); + manager.Setup(m => m.IsLockedOutAsync(user)).Returns(() => Task.FromResult(lockedout)); + manager.Setup(m => m.CheckPasswordAsync(user, "bogus")).ReturnsAsync(false).Verifiable(); + var context = new Mock(); + var helper = SetupSignInManager(manager.Object, context.Object); + + // Act + var result = await helper.CheckPasswordSignInAsync(user, "bogus", true); + + // Assert + Assert.False(result.Succeeded); + Assert.True(result.IsLockedOut); + manager.Verify(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CanRequireConfirmedEmailForPasswordSignIn(bool confirmed) + { + // Setup + var user = new TestUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + manager.Setup(m => m.IsEmailConfirmedAsync(user)).ReturnsAsync(confirmed).Verifiable(); + if (confirmed) + { + manager.Setup(m => m.CheckPasswordAsync(user, "password")).ReturnsAsync(true).Verifiable(); + } + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + if (confirmed) + { + manager.Setup(m => m.CheckPasswordAsync(user, "password")).ReturnsAsync(true).Verifiable(); + SetupSignIn(context, auth); + } + var identityOptions = new IdentityOptions(); + identityOptions.SignIn.RequireConfirmedEmail = true; + var logStore = new StringBuilder(); + var helper = SetupSignInManager(manager.Object, context, logStore, identityOptions); + + // Act + var result = await helper.PasswordSignInAsync(user, "password", false, false); + + // Assert + + Assert.Equal(confirmed, result.Succeeded); + Assert.NotEqual(confirmed, result.IsNotAllowed); + Assert.Equal(confirmed, !logStore.ToString().Contains($"User {user.Id} cannot sign in without a confirmed email.")); + + manager.Verify(); + auth.Verify(); + } + + private static void SetupSignIn(HttpContext context, Mock auth, string userId = null, bool? isPersistent = null, string loginProvider = null) + { + auth.Setup(a => a.SignInAsync(context, + IdentityConstants.ApplicationScheme, + It.Is(id => + (userId == null || id.FindFirstValue(ClaimTypes.NameIdentifier) == userId) && + (loginProvider == null || id.FindFirstValue(ClaimTypes.AuthenticationMethod) == loginProvider)), + It.Is(v => isPersistent == null || v.IsPersistent == isPersistent))).Returns(Task.FromResult(0)).Verifiable(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CanRequireConfirmedPhoneNumberForPasswordSignIn(bool confirmed) + { + // Setup + var user = new TestUser { UserName = "Foo" }; + var manager = SetupUserManager(user); + manager.Setup(m => m.IsPhoneNumberConfirmedAsync(user)).ReturnsAsync(confirmed).Verifiable(); + var context = new DefaultHttpContext(); + var auth = MockAuth(context); + if (confirmed) + { + manager.Setup(m => m.CheckPasswordAsync(user, "password")).ReturnsAsync(true).Verifiable(); + SetupSignIn(context, auth); + } + + var identityOptions = new IdentityOptions(); + identityOptions.SignIn.RequireConfirmedPhoneNumber = true; + var logStore = new StringBuilder(); + var helper = SetupSignInManager(manager.Object, context, logStore, identityOptions); + + // Act + var result = await helper.PasswordSignInAsync(user, "password", false, false); + + // Assert + Assert.Equal(confirmed, result.Succeeded); + Assert.NotEqual(confirmed, result.IsNotAllowed); + Assert.Equal(confirmed, !logStore.ToString().Contains($"User {user.Id} cannot sign in without a confirmed phone number.")); + manager.Verify(); + auth.Verify(); + } + } +} diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/UserClaimsPrincipalFactoryTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/UserClaimsPrincipalFactoryTest.cs new file mode 100644 index 0000000..6feef2b --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/UserClaimsPrincipalFactoryTest.cs @@ -0,0 +1,110 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class UserClaimsPrincipalFactoryTest + { + [Fact] + public async Task CreateIdentityNullChecks() + { + var userManager = MockHelpers.MockUserManager().Object; + var roleManager = MockHelpers.MockRoleManager().Object; + var options = new Mock>(); + Assert.Throws("optionsAccessor", + () => new UserClaimsPrincipalFactory(userManager, roleManager, options.Object)); + var identityOptions = new IdentityOptions(); + options.Setup(a => a.Value).Returns(identityOptions); + var factory = new UserClaimsPrincipalFactory(userManager, roleManager, options.Object); + await Assert.ThrowsAsync("user", + async () => await factory.CreateAsync(null)); + } + + [Theory] + [InlineData(false, false, false)] + [InlineData(false, true, false)] + [InlineData(true, false, false)] + [InlineData(true, true, false)] + [InlineData(true, false, true)] + [InlineData(true, true, true)] + public async Task EnsureClaimsIdentityHasExpectedClaims(bool supportRoles, bool supportClaims, bool supportRoleClaims) + { + // Setup + var userManager = MockHelpers.MockUserManager(); + var roleManager = MockHelpers.MockRoleManager(); + var user = new TestUser { UserName = "Foo" }; + userManager.Setup(m => m.SupportsUserClaim).Returns(supportClaims); + userManager.Setup(m => m.SupportsUserRole).Returns(supportRoles); + userManager.Setup(m => m.GetUserIdAsync(user)).ReturnsAsync(user.Id); + userManager.Setup(m => m.GetUserNameAsync(user)).ReturnsAsync(user.UserName); + var roleClaims = new[] { "Admin", "Local" }; + if (supportRoles) + { + userManager.Setup(m => m.GetRolesAsync(user)).ReturnsAsync(roleClaims); + roleManager.Setup(m => m.SupportsRoleClaims).Returns(supportRoleClaims); + } + var userClaims = new[] { new Claim("Whatever", "Value"), new Claim("Whatever2", "Value2") }; + if (supportClaims) + { + userManager.Setup(m => m.GetClaimsAsync(user)).ReturnsAsync(userClaims); + } + userManager.Object.Options = new IdentityOptions(); + + var admin = new TestRole() { Name = "Admin" }; + var local = new TestRole() { Name = "Local" }; + var adminClaims = new[] { new Claim("AdminClaim1", "Value1"), new Claim("AdminClaim2", "Value2") }; + var localClaims = new[] { new Claim("LocalClaim1", "Value1"), new Claim("LocalClaim2", "Value2") }; + if (supportRoleClaims) + { + roleManager.Setup(m => m.FindByNameAsync("Admin")).ReturnsAsync(admin); + roleManager.Setup(m => m.FindByNameAsync("Local")).ReturnsAsync(local); + roleManager.Setup(m => m.GetClaimsAsync(admin)).ReturnsAsync(adminClaims); + roleManager.Setup(m => m.GetClaimsAsync(local)).ReturnsAsync(localClaims); + } + + var options = new Mock>(); + var identityOptions = new IdentityOptions(); + options.Setup(a => a.Value).Returns(identityOptions); + var factory = new UserClaimsPrincipalFactory(userManager.Object, roleManager.Object, options.Object); + + // Act + var principal = await factory.CreateAsync(user); + var identity = principal.Identities.First(); + + // Assert + var manager = userManager.Object; + Assert.NotNull(identity); + Assert.Single(principal.Identities); + Assert.Equal(IdentityConstants.ApplicationScheme, identity.AuthenticationType); + var claims = identity.Claims.ToList(); + Assert.NotNull(claims); + Assert.Contains(claims, c => c.Type == manager.Options.ClaimsIdentity.UserNameClaimType && c.Value == user.UserName); + Assert.Contains(claims, c => c.Type == manager.Options.ClaimsIdentity.UserIdClaimType && c.Value == user.Id); + Assert.Equal(supportRoles, claims.Any(c => c.Type == manager.Options.ClaimsIdentity.RoleClaimType && c.Value == "Admin")); + Assert.Equal(supportRoles, claims.Any(c => c.Type == manager.Options.ClaimsIdentity.RoleClaimType && c.Value == "Local")); + foreach (var cl in userClaims) + { + Assert.Equal(supportClaims, claims.Any(c => c.Type == cl.Type && c.Value == cl.Value)); + } + foreach (var cl in adminClaims) + { + Assert.Equal(supportRoleClaims, claims.Any(c => c.Type == cl.Type && c.Value == cl.Value)); + } + foreach (var cl in localClaims) + { + Assert.Equal(supportRoleClaims, claims.Any(c => c.Type == cl.Type && c.Value == cl.Value)); + } + userManager.VerifyAll(); + roleManager.VerifyAll(); + } + } +} diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/UserManagerTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/UserManagerTest.cs new file mode 100644 index 0000000..88e4fa4 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/UserManagerTest.cs @@ -0,0 +1,1595 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class UserManagerTest + { + [Fact] + public void EnsureDefaultServicesDefaultsWithStoreWorks() + { + var config = new ConfigurationBuilder().Build(); + var services = new ServiceCollection() + .AddSingleton(config) + .AddTransient, NoopUserStore>(); + services.AddIdentity(); + services.AddSingleton(); + services.AddLogging(); + var manager = services.BuildServiceProvider().GetRequiredService>(); + Assert.NotNull(manager.PasswordHasher); + Assert.NotNull(manager.Options); + } + + [Fact] + public void AddUserManagerWithCustomManagerReturnsSameInstance() + { + var config = new ConfigurationBuilder().Build(); + var services = new ServiceCollection() + .AddSingleton(config) + .AddTransient, NoopUserStore>() + .AddSingleton(); + + services.AddLogging(); + + services.AddIdentity() + .AddUserManager() + .AddRoleManager(); + var provider = services.BuildServiceProvider(); + Assert.Same(provider.GetRequiredService>(), + provider.GetRequiredService()); + Assert.Same(provider.GetRequiredService>(), + provider.GetRequiredService()); + } + + public class CustomUserManager : UserManager + { + public CustomUserManager() : base(new Mock>().Object, null, null, null, null, null, null, null, null) + { } + } + + public class CustomRoleManager : RoleManager + { + public CustomRoleManager() : base(new Mock>().Object, null, null, null, null) + { } + } + + [Fact] + public async Task CreateCallsStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + store.Setup(s => s.CreateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.UserName)).Verifiable(); + store.Setup(s => s.SetNormalizedUserNameAsync(user, user.UserName.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.CreateAsync(user); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task CreateCallsUpdateEmailStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo", Email = "Foo@foo.com" }; + store.Setup(s => s.CreateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.UserName)).Verifiable(); + store.Setup(s => s.GetEmailAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.Email)).Verifiable(); + store.Setup(s => s.SetNormalizedEmailAsync(user, user.Email.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.SetNormalizedUserNameAsync(user, user.UserName.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.CreateAsync(user); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task DeleteCallsStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + store.Setup(s => s.DeleteAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.DeleteAsync(user); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task UpdateCallsStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.UserName)).Verifiable(); + store.Setup(s => s.SetNormalizedUserNameAsync(user, user.UserName.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.UpdateAsync(user); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task UpdateWillUpdateNormalizedEmail() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo", Email = "email" }; + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.UserName)).Verifiable(); + store.Setup(s => s.GetEmailAsync(user, CancellationToken.None)).Returns(Task.FromResult(user.Email)).Verifiable(); + store.Setup(s => s.SetNormalizedUserNameAsync(user, user.UserName.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.SetNormalizedEmailAsync(user, user.Email.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.UpdateAsync(user); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task SetUserNameCallsStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser(); + store.Setup(s => s.SetUserNameAsync(user, "foo", CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)).Returns(Task.FromResult("foo")).Verifiable(); + store.Setup(s => s.SetNormalizedUserNameAsync(user, "FOO", CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.SetUserNameAsync(user, "foo"); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task FindByIdCallsStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + store.Setup(s => s.FindByIdAsync(user.Id, CancellationToken.None)).Returns(Task.FromResult(user)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.FindByIdAsync(user.Id); + + // Assert + Assert.Equal(user, result); + store.VerifyAll(); + } + + [Fact] + public async Task FindByNameCallsStoreWithNormalizedName() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + store.Setup(s => s.FindByNameAsync(user.UserName.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(user)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.FindByNameAsync(user.UserName); + + // Assert + Assert.Equal(user, result); + store.VerifyAll(); + } + + [Fact] + public async Task CanFindByNameCallsStoreWithoutNormalizedName() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + store.Setup(s => s.FindByNameAsync(user.UserName, CancellationToken.None)).Returns(Task.FromResult(user)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + userManager.KeyNormalizer = null; + + // Act + var result = await userManager.FindByNameAsync(user.UserName); + + // Assert + Assert.Equal(user, result); + store.VerifyAll(); + } + + [Fact] + public async Task FindByEmailCallsStoreWithNormalizedEmail() + { + // Setup + var store = new Mock>(); + var user = new TestUser { Email = "Foo" }; + store.Setup(s => s.FindByEmailAsync(user.Email.ToUpperInvariant(), CancellationToken.None)).Returns(Task.FromResult(user)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.FindByEmailAsync(user.Email); + + // Assert + Assert.Equal(user, result); + store.VerifyAll(); + } + + [Fact] + public async Task CanFindByEmailCallsStoreWithoutNormalizedEmail() + { + // Setup + var store = new Mock>(); + var user = new TestUser { Email = "Foo" }; + store.Setup(s => s.FindByEmailAsync(user.Email, CancellationToken.None)).Returns(Task.FromResult(user)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + userManager.KeyNormalizer = null; + + // Act + var result = await userManager.FindByEmailAsync(user.Email); + + // Assert + Assert.Equal(user, result); + store.VerifyAll(); + } + + [Fact] + public async Task AddToRolesCallsStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + var roles = new string[] { "A", "B", "C", "C" }; + store.Setup(s => s.AddToRoleAsync(user, "A", CancellationToken.None)) + .Returns(Task.FromResult(0)) + .Verifiable(); + store.Setup(s => s.AddToRoleAsync(user, "B", CancellationToken.None)) + .Returns(Task.FromResult(0)) + .Verifiable(); + store.Setup(s => s.AddToRoleAsync(user, "C", CancellationToken.None)) + .Returns(Task.FromResult(0)) + .Verifiable(); + + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + store.Setup(s => s.IsInRoleAsync(user, "A", CancellationToken.None)) + .Returns(Task.FromResult(false)) + .Verifiable(); + store.Setup(s => s.IsInRoleAsync(user, "B", CancellationToken.None)) + .Returns(Task.FromResult(false)) + .Verifiable(); + store.Setup(s => s.IsInRoleAsync(user, "C", CancellationToken.None)) + .Returns(Task.FromResult(false)) + .Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.AddToRolesAsync(user, roles); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + store.Verify(s => s.AddToRoleAsync(user, "C", CancellationToken.None), Times.Once()); + } + + [Fact] + public async Task AddToRolesFailsIfUserInRole() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + var roles = new[] { "A", "B", "C" }; + store.Setup(s => s.AddToRoleAsync(user, "A", CancellationToken.None)) + .Returns(Task.FromResult(0)) + .Verifiable(); + store.Setup(s => s.IsInRoleAsync(user, "B", CancellationToken.None)) + .Returns(Task.FromResult(true)) + .Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.AddToRolesAsync(user, roles); + + // Assert + IdentityResultAssert.IsFailure(result, new IdentityErrorDescriber().UserAlreadyInRole("B")); + store.VerifyAll(); + } + + [Fact] + public async Task RemoveFromRolesCallsStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + var roles = new[] { "A", "B", "C" }; + store.Setup(s => s.RemoveFromRoleAsync(user, "A", CancellationToken.None)) + .Returns(Task.FromResult(0)) + .Verifiable(); + store.Setup(s => s.RemoveFromRoleAsync(user, "B", CancellationToken.None)) + .Returns(Task.FromResult(0)) + .Verifiable(); + store.Setup(s => s.RemoveFromRoleAsync(user, "C", CancellationToken.None)) + .Returns(Task.FromResult(0)) + .Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + store.Setup(s => s.IsInRoleAsync(user, "A", CancellationToken.None)) + .Returns(Task.FromResult(true)) + .Verifiable(); + store.Setup(s => s.IsInRoleAsync(user, "B", CancellationToken.None)) + .Returns(Task.FromResult(true)) + .Verifiable(); + store.Setup(s => s.IsInRoleAsync(user, "C", CancellationToken.None)) + .Returns(Task.FromResult(true)) + .Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.RemoveFromRolesAsync(user, roles); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task RemoveFromRolesFailsIfNotInRole() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + var roles = new string[] { "A", "B", "C" }; + store.Setup(s => s.RemoveFromRoleAsync(user, "A", CancellationToken.None)) + .Returns(Task.FromResult(0)) + .Verifiable(); + store.Setup(s => s.IsInRoleAsync(user, "A", CancellationToken.None)) + .Returns(Task.FromResult(true)) + .Verifiable(); + store.Setup(s => s.IsInRoleAsync(user, "B", CancellationToken.None)) + .Returns(Task.FromResult(false)) + .Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.RemoveFromRolesAsync(user, roles); + + // Assert + IdentityResultAssert.IsFailure(result, new IdentityErrorDescriber().UserNotInRole("B")); + store.VerifyAll(); + } + + [Fact] + public async Task AddClaimsCallsStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + var claims = new Claim[] { new Claim("1", "1"), new Claim("2", "2"), new Claim("3", "3") }; + store.Setup(s => s.AddClaimsAsync(user, claims, CancellationToken.None)) + .Returns(Task.FromResult(0)) + .Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.AddClaimsAsync(user, claims); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task AddClaimCallsStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + var claim = new Claim("1", "1"); + store.Setup(s => s.AddClaimsAsync(user, It.IsAny>(), CancellationToken.None)) + .Returns(Task.FromResult(0)) + .Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.AddClaimAsync(user, claim); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task UpdateClaimCallsStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + var claim = new Claim("1", "1"); + var newClaim = new Claim("1", "2"); + store.Setup(s => s.ReplaceClaimAsync(user, It.IsAny(), It.IsAny(), CancellationToken.None)) + .Returns(Task.FromResult(0)) + .Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.ReplaceClaimAsync(user, claim, newClaim); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task CheckPasswordWillRehashPasswordWhenNeeded() + { + // Setup + var store = new Mock>(); + var hasher = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + var pwd = "password"; + var hashed = "hashed"; + var rehashed = "rehashed"; + + store.Setup(s => s.GetPasswordHashAsync(user, CancellationToken.None)) + .ReturnsAsync(hashed) + .Verifiable(); + store.Setup(s => s.SetPasswordHashAsync(user, It.IsAny(), CancellationToken.None)).Returns(Task.FromResult(0)).Verifiable(); + store.Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(IdentityResult.Success)); + + hasher.Setup(s => s.VerifyHashedPassword(user, hashed, pwd)).Returns(PasswordVerificationResult.SuccessRehashNeeded).Verifiable(); + hasher.Setup(s => s.HashPassword(user, pwd)).Returns(rehashed).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + userManager.PasswordHasher = hasher.Object; + + // Act + var result = await userManager.CheckPasswordAsync(user, pwd); + + // Assert + Assert.True(result); + store.VerifyAll(); + hasher.VerifyAll(); + } + + [Fact] + public async Task CreateFailsWithNullSecurityStamp() + { + // Setup + var store = new Mock>(); + var manager = MockHelpers.TestUserManager(store.Object); + var user = new TestUser { UserName = "nulldude" }; + store.Setup(s => s.GetSecurityStampAsync(user, It.IsAny())).ReturnsAsync(default(string)).Verifiable(); + + // Act + // Assert + var ex = await Assert.ThrowsAsync(() => manager.CreateAsync(user)); + //Assert.Contains(Extensions.Identity.Core.Resources.NullSecurityStamp, ex.Message); + + store.VerifyAll(); + } + + [Fact] + public async Task UpdateFailsWithNullSecurityStamp() + { + // Setup + var store = new Mock>(); + var manager = MockHelpers.TestUserManager(store.Object); + var user = new TestUser { UserName = "nulldude" }; + store.Setup(s => s.GetSecurityStampAsync(user, It.IsAny())).ReturnsAsync(default(string)).Verifiable(); + + // Act + // Assert + var ex = await Assert.ThrowsAsync(() => manager.UpdateAsync(user)); + //Assert.Contains(Extensions.Identity.Core.Resources.NullSecurityStamp, ex.Message); + + store.VerifyAll(); + } + + + + [Fact] + public async Task RemoveClaimsCallsStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + var claims = new Claim[] { new Claim("1", "1"), new Claim("2", "2"), new Claim("3", "3") }; + store.Setup(s => s.RemoveClaimsAsync(user, claims, CancellationToken.None)) + .Returns(Task.FromResult(0)) + .Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.RemoveClaimsAsync(user, claims); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task RemoveClaimCallsStore() + { + // Setup + var store = new Mock>(); + var user = new TestUser { UserName = "Foo" }; + var claim = new Claim("1", "1"); + store.Setup(s => s.RemoveClaimsAsync(user, It.IsAny>(), CancellationToken.None)) + .Returns(Task.FromResult(0)) + .Verifiable(); + store.Setup(s => s.UpdateAsync(user, CancellationToken.None)).ReturnsAsync(IdentityResult.Success).Verifiable(); + var userManager = MockHelpers.TestUserManager(store.Object); + + // Act + var result = await userManager.RemoveClaimAsync(user, claim); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task CheckPasswordWithNullUserReturnsFalse() + { + var manager = MockHelpers.TestUserManager(new EmptyStore()); + Assert.False(await manager.CheckPasswordAsync(null, "whatevs")); + } + + [Fact] + public void UsersQueryableFailWhenStoreNotImplemented() + { + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + Assert.False(manager.SupportsQueryableUsers); + Assert.Throws(() => manager.Users.Count()); + } + + [Fact] + public async Task UsersEmailMethodsFailWhenStoreNotImplemented() + { + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + Assert.False(manager.SupportsUserEmail); + await Assert.ThrowsAsync(() => manager.FindByEmailAsync(null)); + await Assert.ThrowsAsync(() => manager.SetEmailAsync(null, null)); + await Assert.ThrowsAsync(() => manager.GetEmailAsync(null)); + await Assert.ThrowsAsync(() => manager.IsEmailConfirmedAsync(null)); + await Assert.ThrowsAsync(() => manager.ConfirmEmailAsync(null, null)); + } + + [Fact] + public async Task UsersPhoneNumberMethodsFailWhenStoreNotImplemented() + { + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + Assert.False(manager.SupportsUserPhoneNumber); + await Assert.ThrowsAsync(async () => await manager.SetPhoneNumberAsync(null, null)); + await Assert.ThrowsAsync(async () => await manager.SetPhoneNumberAsync(null, null)); + await Assert.ThrowsAsync(async () => await manager.GetPhoneNumberAsync(null)); + } + + [Fact] + public async Task TokenMethodsThrowWithNoTokenProvider() + { + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + var user = new TestUser(); + await Assert.ThrowsAsync( + async () => await manager.GenerateUserTokenAsync(user, "bogus", null)); + await Assert.ThrowsAsync( + async () => await manager.VerifyUserTokenAsync(user, "bogus", null, null)); + } + + [Fact] + public async Task PasswordMethodsFailWhenStoreNotImplemented() + { + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + Assert.False(manager.SupportsUserPassword); + await Assert.ThrowsAsync(() => manager.CreateAsync(null, null)); + await Assert.ThrowsAsync(() => manager.ChangePasswordAsync(null, null, null)); + await Assert.ThrowsAsync(() => manager.AddPasswordAsync(null, null)); + await Assert.ThrowsAsync(() => manager.RemovePasswordAsync(null)); + await Assert.ThrowsAsync(() => manager.CheckPasswordAsync(null, null)); + await Assert.ThrowsAsync(() => manager.HasPasswordAsync(null)); + } + + [Fact] + public async Task SecurityStampMethodsFailWhenStoreNotImplemented() + { + var store = new Mock>(); + store.Setup(x => x.GetUserIdAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(Guid.NewGuid().ToString())); + var manager = MockHelpers.TestUserManager(store.Object); + Assert.False(manager.SupportsUserSecurityStamp); + await Assert.ThrowsAsync(() => manager.UpdateSecurityStampAsync(null)); + await Assert.ThrowsAsync(() => manager.GetSecurityStampAsync(null)); + await Assert.ThrowsAsync( + () => manager.VerifyChangePhoneNumberTokenAsync(new TestUser(), "1", "111-111-1111")); + await Assert.ThrowsAsync( + () => manager.GenerateChangePhoneNumberTokenAsync(new TestUser(), "111-111-1111")); + } + + [Fact] + public async Task LoginMethodsFailWhenStoreNotImplemented() + { + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + Assert.False(manager.SupportsUserLogin); + await Assert.ThrowsAsync(async () => await manager.AddLoginAsync(null, null)); + await Assert.ThrowsAsync(async () => await manager.RemoveLoginAsync(null, null, null)); + await Assert.ThrowsAsync(async () => await manager.GetLoginsAsync(null)); + await Assert.ThrowsAsync(async () => await manager.FindByLoginAsync(null, null)); + } + + [Fact] + public async Task ClaimMethodsFailWhenStoreNotImplemented() + { + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + Assert.False(manager.SupportsUserClaim); + await Assert.ThrowsAsync(async () => await manager.AddClaimAsync(null, null)); + await Assert.ThrowsAsync(async () => await manager.ReplaceClaimAsync(null, null, null)); + await Assert.ThrowsAsync(async () => await manager.RemoveClaimAsync(null, null)); + await Assert.ThrowsAsync(async () => await manager.GetClaimsAsync(null)); + } + + private class ATokenProvider : IUserTwoFactorTokenProvider + { + public Task CanGenerateTwoFactorTokenAsync(UserManager manager, TestUser user) + { + throw new NotImplementedException(); + } + + public Task GenerateAsync(string purpose, UserManager manager, TestUser user) + { + throw new NotImplementedException(); + } + + public Task ValidateAsync(string purpose, string token, UserManager manager, TestUser user) + { + throw new NotImplementedException(); + } + } + + [Fact] + public void UserManagerWillUseTokenProviderInstance() + { + var provider = new ATokenProvider(); + var config = new ConfigurationBuilder().Build(); + var services = new ServiceCollection() + .AddSingleton(config) + .AddLogging(); + + services.AddIdentity(o => o.Tokens.ProviderMap.Add("A", new TokenProviderDescriptor(typeof(ATokenProvider)) + { + ProviderInstance = provider + })).AddUserStore(); + var manager = services.BuildServiceProvider().GetService>(); + Assert.ThrowsAsync(() => manager.GenerateUserTokenAsync(new TestUser(), "A", "purpose")); + } + + [Fact] + public void UserManagerWillUseTokenProviderInstanceOverDefaults() + { + var provider = new ATokenProvider(); + var config = new ConfigurationBuilder().Build(); + var services = new ServiceCollection() + .AddSingleton(config) + .AddLogging(); + + services.AddIdentity(o => o.Tokens.ProviderMap.Add(TokenOptions.DefaultProvider, new TokenProviderDescriptor(typeof(ATokenProvider)) + { + ProviderInstance = provider + })).AddUserStore().AddDefaultTokenProviders(); + var manager = services.BuildServiceProvider().GetService>(); + Assert.ThrowsAsync(() => manager.GenerateUserTokenAsync(new TestUser(), TokenOptions.DefaultProvider, "purpose")); + } + + [Fact] + public async Task TwoFactorStoreMethodsFailWhenStoreNotImplemented() + { + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + Assert.False(manager.SupportsUserTwoFactor); + await Assert.ThrowsAsync(async () => await manager.GetTwoFactorEnabledAsync(null)); + await + Assert.ThrowsAsync(async () => await manager.SetTwoFactorEnabledAsync(null, true)); + } + + [Fact] + public async Task LockoutStoreMethodsFailWhenStoreNotImplemented() + { + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + Assert.False(manager.SupportsUserLockout); + await Assert.ThrowsAsync(async () => await manager.GetLockoutEnabledAsync(null)); + await Assert.ThrowsAsync(async () => await manager.SetLockoutEnabledAsync(null, true)); + await Assert.ThrowsAsync(async () => await manager.AccessFailedAsync(null)); + await Assert.ThrowsAsync(async () => await manager.IsLockedOutAsync(null)); + await Assert.ThrowsAsync(async () => await manager.ResetAccessFailedCountAsync(null)); + await Assert.ThrowsAsync(async () => await manager.GetAccessFailedCountAsync(null)); + } + + [Fact] + public async Task RoleMethodsFailWhenStoreNotImplemented() + { + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + Assert.False(manager.SupportsUserRole); + await Assert.ThrowsAsync(async () => await manager.AddToRoleAsync(null, "bogus")); + await Assert.ThrowsAsync(async () => await manager.AddToRolesAsync(null, null)); + await Assert.ThrowsAsync(async () => await manager.GetRolesAsync(null)); + await Assert.ThrowsAsync(async () => await manager.RemoveFromRoleAsync(null, "bogus")); + await Assert.ThrowsAsync(async () => await manager.RemoveFromRolesAsync(null, null)); + await Assert.ThrowsAsync(async () => await manager.IsInRoleAsync(null, "bogus")); + } + + [Fact] + public async Task AuthTokenMethodsFailWhenStoreNotImplemented() + { + + //var error = Extensions.Identity.Core.Resources.StoreNotIUserAuthenticationTokenStore; + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + Assert.False(manager.SupportsUserAuthenticationTokens); + await Assert.ThrowsAsync(async () => await manager.GetAuthenticationTokenAsync(null, null, null)); + await Assert.ThrowsAsync(async () => await manager.SetAuthenticationTokenAsync(null, null, null, null)); + await Assert.ThrowsAsync(async () => await manager.RemoveAuthenticationTokenAsync(null, null, null)); + //await VerifyException(async () => await manager.GetAuthenticationTokenAsync(null, null, null), error); + //await VerifyException(async () => await manager.SetAuthenticationTokenAsync(null, null, null, null), error); + //await VerifyException(async () => await manager.RemoveAuthenticationTokenAsync(null, null, null), error); + } + + [Fact] + public async Task AuthenticatorMethodsFailWhenStoreNotImplemented() + { + //var error = Extensions.Identity.Core.Resources.StoreNotIUserAuthenticatorKeyStore; + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + Assert.False(manager.SupportsUserAuthenticatorKey); + //await VerifyException(async () => await manager.GetAuthenticatorKeyAsync(null), error); + //await VerifyException(async () => await manager.ResetAuthenticatorKeyAsync(null), error); + await Assert.ThrowsAsync(async () => await manager.GetAuthenticatorKeyAsync(null)); + await Assert.ThrowsAsync(async () => await manager.ResetAuthenticatorKeyAsync(null)); + } + + [Fact] + public async Task RecoveryMethodsFailWhenStoreNotImplemented() + { + //var error = Extensions.Identity.Core.Resources.StoreNotIUserTwoFactorRecoveryCodeStore; + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + Assert.False(manager.SupportsUserTwoFactorRecoveryCodes); + //await VerifyException(async () => await manager.RedeemTwoFactorRecoveryCodeAsync(null, null), error); + //await VerifyException(async () => await manager.GenerateNewTwoFactorRecoveryCodesAsync(null, 10), error); + await Assert.ThrowsAsync(async () => await manager.RedeemTwoFactorRecoveryCodeAsync(null, null)); + await Assert.ThrowsAsync(async () => await manager.GenerateNewTwoFactorRecoveryCodesAsync(null, 10)); + } + + private async Task VerifyException(Func code, string expectedMessage) where TException : Exception + { + var error = await Assert.ThrowsAsync(code); + Assert.Equal(expectedMessage, error.Message); + } + + [Fact] + public void DisposeAfterDisposeDoesNotThrow() + { + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + manager.Dispose(); + manager.Dispose(); + } + + [Fact] + public async Task PasswordValidatorBlocksCreate() + { + // TODO: Can switch to Mock eventually + var manager = MockHelpers.TestUserManager(new EmptyStore()); + manager.PasswordValidators.Clear(); + manager.PasswordValidators.Add(new BadPasswordValidator()); + IdentityResultAssert.IsFailure(await manager.CreateAsync(new TestUser(), "password"), + BadPasswordValidator.ErrorMessage); + } + + [Fact] + public async Task ResetTokenCallNoopForTokenValueZero() + { + var user = new TestUser() { UserName = Guid.NewGuid().ToString() }; + var store = new Mock>(); + store.Setup(x => x.ResetAccessFailedCountAsync(user, It.IsAny())).Returns(() => + { + throw new Exception(); + }); + var manager = MockHelpers.TestUserManager(store.Object); + + IdentityResultAssert.IsSuccess(await manager.ResetAccessFailedCountAsync(user)); + } + + [Fact] + public async Task ManagerPublicNullChecks() + { + Assert.Throws("store", + () => new UserManager(null, null, null, null, null, null, null, null, null)); + + var manager = MockHelpers.TestUserManager(new NotImplementedStore()); + + await Assert.ThrowsAsync("user", async () => await manager.CreateAsync(null)); + await Assert.ThrowsAsync("user", async () => await manager.CreateAsync(null, null)); + await + Assert.ThrowsAsync("password", + async () => await manager.CreateAsync(new TestUser(), null)); + await Assert.ThrowsAsync("user", async () => await manager.UpdateAsync(null)); + await Assert.ThrowsAsync("user", async () => await manager.DeleteAsync(null)); + await Assert.ThrowsAsync("claim", async () => await manager.AddClaimAsync(null, null)); + await Assert.ThrowsAsync("claim", async () => await manager.ReplaceClaimAsync(null, null, null)); + await Assert.ThrowsAsync("claims", async () => await manager.AddClaimsAsync(null, null)); + await Assert.ThrowsAsync("userName", async () => await manager.FindByNameAsync(null)); + await Assert.ThrowsAsync("login", async () => await manager.AddLoginAsync(null, null)); + await Assert.ThrowsAsync("loginProvider", + async () => await manager.RemoveLoginAsync(null, null, null)); + await Assert.ThrowsAsync("providerKey", + async () => await manager.RemoveLoginAsync(null, "", null)); + await Assert.ThrowsAsync("email", async () => await manager.FindByEmailAsync(null)); + Assert.Throws("provider", () => manager.RegisterTokenProvider("whatever", null)); + await Assert.ThrowsAsync("roles", async () => await manager.AddToRolesAsync(new TestUser(), null)); + await Assert.ThrowsAsync("roles", async () => await manager.RemoveFromRolesAsync(new TestUser(), null)); + } + + [Fact] + public async Task MethodsFailWithUnknownUserTest() + { + var manager = MockHelpers.TestUserManager(new EmptyStore()); + manager.RegisterTokenProvider("whatever", new NoOpTokenProvider()); + await Assert.ThrowsAsync("user", + async () => await manager.GetUserNameAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.SetUserNameAsync(null, "bogus")); + await Assert.ThrowsAsync("user", + async () => await manager.AddClaimAsync(null, new Claim("a", "b"))); + await Assert.ThrowsAsync("user", + async () => await manager.AddLoginAsync(null, new UserLoginInfo("", "", ""))); + await Assert.ThrowsAsync("user", + async () => await manager.AddPasswordAsync(null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.AddToRoleAsync(null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.AddToRolesAsync(null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.ChangePasswordAsync(null, null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.GetClaimsAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.GetLoginsAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.GetRolesAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.IsInRoleAsync(null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.RemoveClaimAsync(null, new Claim("a", "b"))); + await Assert.ThrowsAsync("user", + async () => await manager.RemoveLoginAsync(null, "", "")); + await Assert.ThrowsAsync("user", + async () => await manager.RemovePasswordAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.RemoveFromRoleAsync(null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.RemoveFromRolesAsync(null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.ReplaceClaimAsync(null, new Claim("a", "b"), new Claim("a", "c"))); + await Assert.ThrowsAsync("user", + async () => await manager.UpdateSecurityStampAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.GetSecurityStampAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.HasPasswordAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.GeneratePasswordResetTokenAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.ResetPasswordAsync(null, null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.IsEmailConfirmedAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.GenerateEmailConfirmationTokenAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.ConfirmEmailAsync(null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.GetEmailAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.SetEmailAsync(null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.IsPhoneNumberConfirmedAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.ChangePhoneNumberAsync(null, null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.VerifyChangePhoneNumberTokenAsync(null, null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.GetPhoneNumberAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.SetPhoneNumberAsync(null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.GetTwoFactorEnabledAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.SetTwoFactorEnabledAsync(null, true)); + await Assert.ThrowsAsync("user", + async () => await manager.GenerateTwoFactorTokenAsync(null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.VerifyTwoFactorTokenAsync(null, null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.GetValidTwoFactorProvidersAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.VerifyUserTokenAsync(null, null, null, null)); + await Assert.ThrowsAsync("user", + async () => await manager.AccessFailedAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.ResetAccessFailedCountAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.GetAccessFailedCountAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.GetLockoutEnabledAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.SetLockoutEnabledAsync(null, false)); + await Assert.ThrowsAsync("user", + async () => await manager.SetLockoutEndDateAsync(null, DateTimeOffset.UtcNow)); + await Assert.ThrowsAsync("user", + async () => await manager.GetLockoutEndDateAsync(null)); + await Assert.ThrowsAsync("user", + async () => await manager.IsLockedOutAsync(null)); + } + + [Fact] + public async Task MethodsThrowWhenDisposedTest() + { + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + manager.Dispose(); + await Assert.ThrowsAsync(() => manager.AddClaimAsync(null, null)); + await Assert.ThrowsAsync(() => manager.AddClaimsAsync(null, null)); + await Assert.ThrowsAsync(() => manager.AddLoginAsync(null, null)); + await Assert.ThrowsAsync(() => manager.AddPasswordAsync(null, null)); + await Assert.ThrowsAsync(() => manager.AddToRoleAsync(null, null)); + await Assert.ThrowsAsync(() => manager.AddToRolesAsync(null, null)); + await Assert.ThrowsAsync(() => manager.ChangePasswordAsync(null, null, null)); + await Assert.ThrowsAsync(() => manager.GetClaimsAsync(null)); + await Assert.ThrowsAsync(() => manager.GetLoginsAsync(null)); + await Assert.ThrowsAsync(() => manager.GetRolesAsync(null)); + await Assert.ThrowsAsync(() => manager.IsInRoleAsync(null, null)); + await Assert.ThrowsAsync(() => manager.RemoveClaimAsync(null, null)); + await Assert.ThrowsAsync(() => manager.RemoveClaimsAsync(null, null)); + await Assert.ThrowsAsync(() => manager.RemoveLoginAsync(null, null, null)); + await Assert.ThrowsAsync(() => manager.RemovePasswordAsync(null)); + await Assert.ThrowsAsync(() => manager.RemoveFromRoleAsync(null, null)); + await Assert.ThrowsAsync(() => manager.RemoveFromRolesAsync(null, null)); + await Assert.ThrowsAsync(() => manager.FindByLoginAsync(null, null)); + await Assert.ThrowsAsync(() => manager.FindByIdAsync(null)); + await Assert.ThrowsAsync(() => manager.FindByNameAsync(null)); + await Assert.ThrowsAsync(() => manager.CreateAsync(null)); + await Assert.ThrowsAsync(() => manager.CreateAsync(null, null)); + await Assert.ThrowsAsync(() => manager.UpdateAsync(null)); + await Assert.ThrowsAsync(() => manager.DeleteAsync(null)); + await Assert.ThrowsAsync(() => manager.ReplaceClaimAsync(null, null, null)); + await Assert.ThrowsAsync(() => manager.UpdateSecurityStampAsync(null)); + await Assert.ThrowsAsync(() => manager.GetSecurityStampAsync(null)); + await Assert.ThrowsAsync(() => manager.GeneratePasswordResetTokenAsync(null)); + await Assert.ThrowsAsync(() => manager.ResetPasswordAsync(null, null, null)); + await Assert.ThrowsAsync(() => manager.GenerateEmailConfirmationTokenAsync(null)); + await Assert.ThrowsAsync(() => manager.IsEmailConfirmedAsync(null)); + await Assert.ThrowsAsync(() => manager.ConfirmEmailAsync(null, null)); + } + + private class BadPasswordValidator : IPasswordValidator where TUser : class + { + public static readonly IdentityError ErrorMessage = new IdentityError { Description = "I'm Bad." }; + + public Task ValidateAsync(UserManager manager, TUser user, string password) + { + return Task.FromResult(IdentityResult.Failed(ErrorMessage)); + } + } + + private class EmptyStore : + IUserPasswordStore, + IUserClaimStore, + IUserLoginStore, + IUserEmailStore, + IUserPhoneNumberStore, + IUserLockoutStore, + IUserTwoFactorStore, + IUserRoleStore, + IUserSecurityStampStore + { + public Task> GetClaimsAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult>(new List()); + } + + public Task AddClaimsAsync(TestUser user, IEnumerable claim, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task ReplaceClaimAsync(TestUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task RemoveClaimsAsync(TestUser user, IEnumerable claim, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task SetEmailAsync(TestUser user, string email, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task GetEmailAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(""); + } + + public Task GetEmailConfirmedAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(false); + } + + public Task SetEmailConfirmedAsync(TestUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public Task GetLockoutEndDateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(DateTimeOffset.MinValue); + } + + public Task SetLockoutEndDateAsync(TestUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task IncrementAccessFailedCountAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task ResetAccessFailedCountAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task GetAccessFailedCountAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task GetLockoutEnabledAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(false); + } + + public Task SetLockoutEnabledAsync(TestUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task AddLoginAsync(TestUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task RemoveLoginAsync(TestUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task> GetLoginsAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult>(new List()); + } + + public Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public void Dispose() + { + } + + public Task SetUserNameAsync(TestUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task CreateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(IdentityResult.Success); + } + + public Task UpdateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(IdentityResult.Success); + } + + public Task DeleteAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(IdentityResult.Success); + } + + public Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public Task FindByNameAsync(string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public Task SetPasswordHashAsync(TestUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task GetPasswordHashAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public Task HasPasswordAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(false); + } + + public Task SetPhoneNumberAsync(TestUser user, string phoneNumber, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task GetPhoneNumberAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(""); + } + + public Task GetPhoneNumberConfirmedAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(false); + } + + public Task SetPhoneNumberConfirmedAsync(TestUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task AddToRoleAsync(TestUser user, string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task RemoveFromRoleAsync(TestUser user, string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task> GetRolesAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult>(new List()); + } + + public Task IsInRoleAsync(TestUser user, string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(false); + } + + public Task SetSecurityStampAsync(TestUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task GetSecurityStampAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(""); + } + + public Task SetTwoFactorEnabledAsync(TestUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task GetTwoFactorEnabledAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(false); + } + + public Task GetUserIdAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public Task GetUserNameAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public Task GetNormalizedUserNameAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(null); + } + + public Task SetNormalizedUserNameAsync(TestUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + + public Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult>(new List()); + } + + public Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult>(new List()); + } + + public Task GetNormalizedEmailAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(""); + } + + public Task SetNormalizedEmailAsync(TestUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(0); + } + } + + private class NoOpTokenProvider : IUserTwoFactorTokenProvider + { + public string Name { get; } = "Noop"; + + public Task GenerateAsync(string purpose, UserManager manager, TestUser user) + { + return Task.FromResult("Test"); + } + + public Task ValidateAsync(string purpose, string token, UserManager manager, TestUser user) + { + return Task.FromResult(true); + } + + public Task CanGenerateTwoFactorTokenAsync(UserManager manager, TestUser user) + { + return Task.FromResult(true); + } + } + + private class NotImplementedStore : + IUserPasswordStore, + IUserClaimStore, + IUserLoginStore, + IUserRoleStore, + IUserEmailStore, + IUserPhoneNumberStore, + IUserLockoutStore, + IUserTwoFactorStore + { + public Task> GetClaimsAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task AddClaimsAsync(TestUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task ReplaceClaimAsync(TestUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task RemoveClaimsAsync(TestUser user, IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetEmailAsync(TestUser user, string email, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetEmailAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetEmailConfirmedAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetEmailConfirmedAsync(TestUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetLockoutEndDateAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetLockoutEndDateAsync(TestUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task IncrementAccessFailedCountAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task ResetAccessFailedCountAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetAccessFailedCountAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetLockoutEnabledAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetLockoutEnabledAsync(TestUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task AddLoginAsync(TestUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task RemoveLoginAsync(TestUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task> GetLoginsAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + throw new NotImplementedException(); + } + + public Task GetUserIdAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetUserNameAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetUserNameAsync(TestUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task FindByNameAsync(string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetPasswordHashAsync(TestUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetPasswordHashAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task HasPasswordAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetPhoneNumberAsync(TestUser user, string phoneNumber, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetPhoneNumberAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetPhoneNumberConfirmedAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetPhoneNumberConfirmedAsync(TestUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetTwoFactorEnabledAsync(TestUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetTwoFactorEnabledAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task AddToRoleAsync(TestUser user, string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task RemoveFromRoleAsync(TestUser user, string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task> GetRolesAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task IsInRoleAsync(TestUser user, string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task GetNormalizedUserNameAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedUserNameAsync(TestUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + Task IUserStore.CreateAsync(TestUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + Task IUserStore.UpdateAsync(TestUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + Task IUserStore.DeleteAsync(TestUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetNormalizedEmailAsync(TestUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedEmailAsync(TestUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + throw new NotImplementedException(); + } + } + + [Fact] + public async Task CanCustomizeUserValidatorErrors() + { + var store = new Mock>(); + var describer = new TestErrorDescriber(); + var config = new ConfigurationBuilder().Build(); + var services = new ServiceCollection() + .AddSingleton(config) + .AddLogging() + .AddSingleton(describer) + .AddSingleton>(store.Object) + .AddSingleton(); + + services.AddIdentity(); + + var manager = services.BuildServiceProvider().GetRequiredService>(); + + manager.Options.User.RequireUniqueEmail = true; + var user = new TestUser() { UserName = "dupeEmail", Email = "dupe@email.com" }; + var user2 = new TestUser() { UserName = "dupeEmail2", Email = "dupe@email.com" }; + store.Setup(s => s.FindByEmailAsync("DUPE@EMAIL.COM", CancellationToken.None)) + .Returns(Task.FromResult(user2)) + .Verifiable(); + store.Setup(s => s.GetUserIdAsync(user2, CancellationToken.None)) + .Returns(Task.FromResult(user2.Id)) + .Verifiable(); + store.Setup(s => s.GetUserNameAsync(user, CancellationToken.None)) + .Returns(Task.FromResult(user.UserName)) + .Verifiable(); + store.Setup(s => s.GetEmailAsync(user, CancellationToken.None)) + .Returns(Task.FromResult(user.Email)) + .Verifiable(); + + Assert.Same(describer, manager.ErrorDescriber); + IdentityResultAssert.IsFailure(await manager.CreateAsync(user), describer.DuplicateEmail(user.Email)); + + store.VerifyAll(); + } + + public class TestErrorDescriber : IdentityErrorDescriber + { + public static string Code = "Error"; + public static string FormatError = "FormatError {0}"; + + public override IdentityError DuplicateEmail(string email) + { + return new IdentityError { Code = Code, Description = string.Format(FormatError, email) }; + } + } + + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/UserValidatorTest.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/UserValidatorTest.cs new file mode 100644 index 0000000..eadafb8 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/AspNetCore.Identity.Test/UserValidatorTest.cs @@ -0,0 +1,99 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public class UserValidatorTest + { + [Fact] + public async Task ValidateThrowsWithNull() + { + // Setup + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + var validator = new UserValidator(); + + // Act + // Assert + await Assert.ThrowsAsync("manager", () => validator.ValidateAsync(null, null)); + await Assert.ThrowsAsync("user", () => validator.ValidateAsync(manager, null)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public async Task ValidateFailsWithTooShortUserNames(string input) + { + // Setup + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + var validator = new UserValidator(); + var user = new TestUser {UserName = input}; + + // Act + var result = await validator.ValidateAsync(manager, user); + + // Assert + IdentityResultAssert.IsFailure(result, new IdentityErrorDescriber().InvalidUserName(input)); + } + + [Theory] + [InlineData("test_email@foo.com", true)] + [InlineData("hao", true)] + [InlineData("test123", true)] + [InlineData("hyphen-yes@foo-bar.com", true)] + [InlineData("+plus+yes+@foo-bar.com", true)] + [InlineData("!noway", false)] + [InlineData("foo@boz#.com", false)] + public async Task DefaultAlphaNumericOnlyUserNameValidation(string userName, bool expectSuccess) + { + // Setup + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + var validator = new UserValidator(); + var user = new TestUser {UserName = userName}; + + // Act + var result = await validator.ValidateAsync(manager, user); + + // Assert + if (expectSuccess) + { + IdentityResultAssert.IsSuccess(result); + } + else + { + IdentityResultAssert.IsFailure(result); + } + } + + [Theory] + [InlineData("test_email@foo.com", true)] + [InlineData("hao", true)] + [InlineData("test123", true)] + [InlineData("!noway", true)] + [InlineData("foo@boz#.com", true)] + public async Task CanAllowNonAlphaNumericUserName(string userName, bool expectSuccess) + { + // Setup + var manager = MockHelpers.TestUserManager(new NoopUserStore()); + manager.Options.User.AllowedUserNameCharacters = null; + var validator = new UserValidator(); + var user = new TestUser {UserName = userName}; + + // Act + var result = await validator.ValidateAsync(manager, user); + + // Assert + if (expectSuccess) + { + IdentityResultAssert.IsSuccess(result); + } + else + { + IdentityResultAssert.IsFailure(result); + } + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Infrastructure/Container.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Infrastructure/Container.cs new file mode 100644 index 0000000..c2e3de0 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Infrastructure/Container.cs @@ -0,0 +1,68 @@ +using AspNetCore.Identity.MongoDbCore.Extensions; +using AspNetCore.Identity.MongoDbCore.Infrastructure; +using System; + +namespace AspNetCore.Identity.MongoDbCore.IntegrationTests.Infrastructure +{ + public static class Locks + { + public static object MongoInitLock = new object(); + public static object RolesLock = new object(); + } + + public static class Container + { + public static MongoDbIdentityConfiguration MongoDbIdentityConfiguration = new MongoDbIdentityConfiguration + { + MongoDbSettings = new MongoDbSettings + { + ConnectionString = "mongodb://localhost:27017", + DatabaseName = "MongoDbTests" + }, + IdentityOptionsAction = options => + { + options.Password.RequireDigit = false; + options.Password.RequireLowercase = false; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + options.User.AllowedUserNameCharacters = null; + } + }; + + public static IServiceProvider Instance { get; set; } + + const string connectionString = "mongodb://localhost:27017"; + private static readonly IMongoRepository _mongoDbRepository; + + private static readonly IMongoRepository _mongoDbRepository2; + + static Container() + { + lock (Locks.MongoInitLock) + { + _mongoDbRepository = new MongoRepository( + MongoDbIdentityConfiguration.MongoDbSettings.ConnectionString, + MongoDbIdentityConfiguration.MongoDbSettings.DatabaseName); + _mongoDbRepository2 = new MongoRepository( + MongoDbIdentityConfiguration.MongoDbSettings.ConnectionString, + MongoDbIdentityConfiguration.MongoDbSettings.DatabaseName); + } + } + + public static IMongoRepository MongoRepository + { + get + { + return _mongoDbRepository; + } + } + + public static IMongoRepository MongoRepositoryConcurrent + { + get + { + return _mongoDbRepository2; + } + } + } +} diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Infrastructure/TestMongoIdentityRole.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Infrastructure/TestMongoIdentityRole.cs new file mode 100644 index 0000000..79c2b18 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Infrastructure/TestMongoIdentityRole.cs @@ -0,0 +1,18 @@ +using AspNetCore.Identity.MongoDbCore.Models; +using System; + +namespace AspNetCore.Identity.MongoDbCore.IntegrationTests.Infrastructure +{ + public class TestMongoIdentityRole : MongoIdentityRole + { + public TestMongoIdentityRole() : base () + { + Id = Guid.NewGuid(); + } + + public TestMongoIdentityRole(string roleName) : base(roleName) + { + Id = Guid.NewGuid(); + } + } +} diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Infrastructure/TestMongoIdentityUser.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Infrastructure/TestMongoIdentityUser.cs new file mode 100644 index 0000000..fbadbe9 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Infrastructure/TestMongoIdentityUser.cs @@ -0,0 +1,25 @@ +using AspNetCore.Identity.MongoDbCore.Models; +using System; + +namespace AspNetCore.Identity.MongoDbCore.IntegrationTests.Infrastructure +{ + public class TestMongoIdentityUser : MongoIdentityUser + { + public TestMongoIdentityUser() : base() + { + Id = Guid.NewGuid(); + } + + public TestMongoIdentityUser(string userName) : base(userName) + { + Id = Guid.NewGuid(); + } + + public TestMongoIdentityUser(string userName, string email) : base(userName, email) + { + Id = Guid.NewGuid(); + } + + public string CustomContent { get; set; } + } +} diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/ApiConsistencyTestBase.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/ApiConsistencyTestBase.cs new file mode 100644 index 0000000..29d75df --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/ApiConsistencyTestBase.cs @@ -0,0 +1,97 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public abstract class ApiConsistencyTestBase + { + [Fact] + public void Public_inheritable_apis_should_be_virtual() + { + var nonVirtualMethods + = (from type in GetAllTypes(TargetAssembly.DefinedTypes) + where type.IsVisible + && !type.IsSealed + && type.DeclaredConstructors.Any(c => c.IsPublic || c.IsFamily || c.IsFamilyOrAssembly) + && type.Namespace != null + && !type.Namespace.EndsWith(".Compiled") + from method in type.DeclaredMethods.Where(m => m.IsPublic && !m.IsStatic) + where GetBasestTypeInAssembly(method.DeclaringType) == type + && !(method.IsVirtual && !method.IsFinal) + && !method.Name.StartsWith("get_") + && !method.Name.StartsWith("set_") + && !method.Name.Equals("Dispose") + select type.Name + "." + method.Name) + .ToList(); + + Assert.False( + nonVirtualMethods.Any(), + "\r\n-- Missing virtual APIs --\r\n" + string.Join("\r\n", nonVirtualMethods)); + } + + [Fact] + public void Async_methods_should_end_with_async_suffix() + { + var asyncMethods + = (from type in GetAllTypes(TargetAssembly.DefinedTypes) + where type.IsVisible + from method in type.DeclaredMethods.Where(m => m.IsPublic) + where GetBasestTypeInAssembly(method.DeclaringType) == type + where typeof(Task).IsAssignableFrom(method.ReturnType) + select method).ToList(); + + var missingSuffixMethods + = asyncMethods + .Where(method => !method.Name.EndsWith("Async")) + .Select(method => method.DeclaringType.Name + "." + method.Name) + .Except(GetAsyncSuffixExceptions()) + .ToList(); + + Assert.False( + missingSuffixMethods.Any(), + "\r\n-- Missing async suffix --\r\n" + string.Join("\r\n", missingSuffixMethods)); + } + + protected virtual IEnumerable GetCancellationTokenExceptions() + { + return Enumerable.Empty(); + } + + protected virtual IEnumerable GetAsyncSuffixExceptions() + { + return Enumerable.Empty(); + } + + protected abstract Assembly TargetAssembly { get; } + + protected virtual IEnumerable GetAllTypes(IEnumerable types) + { + foreach (var type in types) + { + yield return type; + + foreach (var nestedType in GetAllTypes(type.DeclaredNestedTypes)) + { + yield return nestedType; + } + } + } + + protected TypeInfo GetBasestTypeInAssembly(Type type) + { + while (type.GetTypeInfo()?.BaseType?.GetTypeInfo()?.Assembly == type.GetTypeInfo().Assembly) + { + type = type.GetTypeInfo().BaseType; + } + + return type.GetTypeInfo(); + } + } +} diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/MockHelpers.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/MockHelpers.cs new file mode 100644 index 0000000..1b563e1 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/MockHelpers.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; + +namespace Microsoft.AspNetCore.Identity.Test +{ + public static class MockHelpers + { + public static StringBuilder LogMessage = new StringBuilder(); + + public static Mock> MockUserManager() where TUser : class + { + var store = new Mock>(); + var mgr = new Mock>(store.Object, null, null, null, null, null, null, null, null); + mgr.Object.UserValidators.Add(new UserValidator()); + mgr.Object.PasswordValidators.Add(new PasswordValidator()); + return mgr; + } + + public static Mock> MockRoleManager(IRoleStore store = null) where TRole : class + { + store = store ?? new Mock>().Object; + var roles = new List>(); + roles.Add(new RoleValidator()); + return new Mock>(store, roles, new UpperInvariantLookupNormalizer(), + new IdentityErrorDescriber(), null); + } + + public static Mock> MockILogger(StringBuilder logStore = null) where T : class + { + logStore = logStore ?? LogMessage; + var logger = new Mock>(); + logger.Setup(x => x.Log(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>())) + .Callback((LogLevel logLevel, EventId eventId, object state, Exception exception, Func formatter) => + { + if (formatter == null) + { + logStore.Append(state.ToString()); + } + else + { + logStore.Append(formatter(state, exception)); + } + logStore.Append(" "); + }); + logger.Setup(x => x.BeginScope(It.IsAny())).Callback((object state) => + { + logStore.Append(state.ToString()); + logStore.Append(" "); + }); + logger.Setup(x => x.IsEnabled(LogLevel.Debug)).Returns(true); + logger.Setup(x => x.IsEnabled(LogLevel.Warning)).Returns(true); + + return logger; + } + + public static UserManager TestUserManager(IUserStore store = null) where TUser : class + { + store = store ?? new Mock>().Object; + var options = new Mock>(); + var idOptions = new IdentityOptions(); + idOptions.Lockout.AllowedForNewUsers = false; + options.Setup(o => o.Value).Returns(idOptions); + var userValidators = new List>(); + var validator = new Mock>(); + userValidators.Add(validator.Object); + var pwdValidators = new List>(); + pwdValidators.Add(new PasswordValidator()); + var userManager = new UserManager(store, options.Object, new PasswordHasher(), + userValidators, pwdValidators, new UpperInvariantLookupNormalizer(), + new IdentityErrorDescriber(), null, + new Mock>>().Object); + validator.Setup(v => v.ValidateAsync(userManager, It.IsAny())) + .Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); + return userManager; + } + + public static RoleManager TestRoleManager(IRoleStore store = null) where TRole : class + { + store = store ?? new Mock>().Object; + var roles = new List>(); + roles.Add(new RoleValidator()); + return new AspNetRoleManager(store, roles, + new UpperInvariantLookupNormalizer(), + new IdentityErrorDescriber(), + null, + null); + } + + } +} diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/PriorityOrderer.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/PriorityOrderer.cs new file mode 100644 index 0000000..1e43b11 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/PriorityOrderer.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity.Test +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Xunit.Abstractions; + using Xunit.Sdk; + + /// + /// Test priority + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class TestPriorityAttribute : Attribute + { + /// + /// ctor + /// + /// + public TestPriorityAttribute(int priority) + { + Priority = priority; + } + + /// + /// Priority + /// + public int Priority { get; private set; } + } + + /// + /// Used to run tests in order. + /// + public class PriorityOrderer : ITestCaseOrderer + { + /// + /// Orders tests cases + /// + /// + /// + /// + public IEnumerable OrderTestCases(IEnumerable testCases) where XunitTestCase : ITestCase + { + var sortedMethods = new SortedDictionary>(); + + foreach (XunitTestCase testCase in testCases) + { + int priority = 0; + + foreach (IAttributeInfo attr in testCase.TestMethod.Method.GetCustomAttributes((typeof(TestPriorityAttribute)).AssemblyQualifiedName)) + priority = attr.GetNamedArgument("Priority"); + + GetOrCreate(sortedMethods, priority).Add(testCase); + } + + foreach (var list in sortedMethods.Keys.Select(priority => sortedMethods[priority])) + { + list.Sort((x, y) => StringComparer.OrdinalIgnoreCase.Compare(x.TestMethod.Method.Name, y.TestMethod.Method.Name)); + foreach (XunitTestCase testCase in list) + yield return testCase; + } + } + + static TValue GetOrCreate(IDictionary dictionary, TKey key) where TValue : new() + { + TValue result; + + if (dictionary.TryGetValue(key, out result)) return result; + + result = new TValue(); + dictionary[key] = result; + + return result; + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestRole.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestRole.cs new file mode 100644 index 0000000..ea1dbf0 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestRole.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Identity.Test +{ + /// + /// Represents a Role entity + /// + public class TestRole : TestRole + { + /// + /// Constructor + /// + public TestRole() + { + Id = Guid.NewGuid().ToString(); + } + + /// + /// Constructor + /// + /// + public TestRole(string roleName) : this() + { + Name = roleName; + } + } + + /// + /// Represents a Role entity + /// + /// + public class TestRole where TKey : IEquatable + { + /// + /// Constructor + /// + public TestRole() { } + + /// + /// Constructor + /// + /// + public TestRole(string roleName) : this() + { + Name = roleName; + } + + /// + /// Role id + /// + public virtual TKey Id { get; set; } + + /// + /// Navigation property for claims in the role + /// + public virtual ICollection> Claims { get; private set; } = new List>(); + + /// + /// Role name + /// + public virtual string Name { get; set; } + + /// + /// Normalized name used for equality + /// + public virtual string NormalizedName { get; set; } + + /// + /// A random value that should change whenever a role is persisted to the store + /// + public virtual string ConcurrencyStamp { get; set; } = Guid.NewGuid().ToString(); + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestRoleClaim.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestRoleClaim.cs new file mode 100644 index 0000000..3f7a3ce --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestRoleClaim.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Identity.Test +{ + /// + /// EntityType that represents one specific role claim + /// + public class TestRoleClaim : TestRoleClaim { } + + /// + /// EntityType that represents one specific role claim + /// + /// + public class TestRoleClaim where TKey : IEquatable + { + /// + /// Primary key + /// + public virtual int Id { get; set; } + + /// + /// User Id for the role this claim belongs to + /// + public virtual TKey RoleId { get; set; } + + /// + /// Claim type + /// + public virtual string ClaimType { get; set; } + + /// + /// Claim value + /// + public virtual string ClaimValue { get; set; } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestUser.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestUser.cs new file mode 100644 index 0000000..d08e747 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestUser.cs @@ -0,0 +1,144 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Identity.Test +{ + /// + /// Test user class + /// + public class TestUser : TestUser + { + /// + /// Ctor + /// + public TestUser() + { + Id = Guid.NewGuid().ToString(); + } + + /// + /// Ctor + /// + /// + public TestUser(string userName) : this() + { + UserName = userName; + } + } + + /// + /// Test user + /// + /// + public class TestUser where TKey : IEquatable + { + /// + /// ctor + /// + public TestUser() { } + + /// + /// ctor + /// + /// + public TestUser(string userName) : this() + { + UserName = userName; + } + + /// + /// Id + /// + public virtual TKey Id { get; set; } + + /// + /// Name + /// + public virtual string UserName { get; set; } + + /// + /// normalized user name + /// + public virtual string NormalizedUserName { get; set; } + + /// + /// Email + /// + public virtual string Email { get; set; } + + /// + /// normalized email + /// + public virtual string NormalizedEmail { get; set; } + + /// + /// True if the email is confirmed, default is false + /// + public virtual bool EmailConfirmed { get; set; } + + /// + /// The salted/hashed form of the user password + /// + public virtual string PasswordHash { get; set; } + + /// + /// A random value that should change whenever a users credentials change (password changed, login removed) + /// + public virtual string SecurityStamp { get; set; } + + /// + /// A random value that should change whenever a user is persisted to the store + /// + public virtual string ConcurrencyStamp { get; set; } = Guid.NewGuid().ToString(); + + /// + /// PhoneNumber for the user + /// + public virtual string PhoneNumber { get; set; } + + /// + /// True if the phone number is confirmed, default is false + /// + public virtual bool PhoneNumberConfirmed { get; set; } + + /// + /// Is two factor enabled for the user + /// + public virtual bool TwoFactorEnabled { get; set; } + + /// + /// DateTime in UTC when lockout ends, any time in the past is considered not locked out. + /// + public virtual DateTimeOffset? LockoutEnd { get; set; } + + /// + /// Is lockout enabled for this user + /// + public virtual bool LockoutEnabled { get; set; } + + /// + /// Used to record failures for the purposes of lockout + /// + public virtual int AccessFailedCount { get; set; } + + /// + /// Navigation property + /// + public virtual ICollection> Roles { get; private set; } = new List>(); + /// + /// Navigation property + /// + public virtual ICollection> Claims { get; private set; } = new List>(); + /// + /// Navigation property + /// + public virtual ICollection> Logins { get; private set; } = new List>(); + /// + /// Navigation property + /// + public virtual ICollection> Tokens { get; private set; } = new List>(); + } +} diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestUserClaim.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestUserClaim.cs new file mode 100644 index 0000000..cac773f --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestUserClaim.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Identity.Test +{ + /// + /// EntityType that represents one specific user claim + /// + public class TestUserClaim : TestUserClaim { } + + /// + /// EntityType that represents one specific user claim + /// + /// + public class TestUserClaim where TKey : IEquatable + { + /// + /// Primary key + /// + public virtual int Id { get; set; } + + /// + /// User Id for the user who owns this claim + /// + public virtual TKey UserId { get; set; } + + /// + /// Claim type + /// + public virtual string ClaimType { get; set; } + + /// + /// Claim value + /// + public virtual string ClaimValue { get; set; } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestUserLogin.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestUserLogin.cs new file mode 100644 index 0000000..43d3b31 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestUserLogin.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Identity.Test +{ + /// + /// Entity type for a user's login (i.e. facebook, google) + /// + public class TestUserLogin : TestUserLogin { } + + /// + /// Entity type for a user's login (i.e. facebook, google) + /// + /// + public class TestUserLogin where TKey : IEquatable + { + /// + /// The login provider for the login (i.e. facebook, google) + /// + public virtual string LoginProvider { get; set; } + + /// + /// Key representing the login for the provider + /// + public virtual string ProviderKey { get; set; } + + /// + /// Display name for the login + /// + public virtual string ProviderDisplayName { get; set; } + + /// + /// User Id for the user who owns this login + /// + public virtual TKey UserId { get; set; } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestUserRole.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestUserRole.cs new file mode 100644 index 0000000..906cff9 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestUserRole.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Identity.Test +{ + /// + /// EntityType that represents a user belonging to a role + /// + public class TestUserRole : TestUserRole { } + + /// + /// EntityType that represents a user belonging to a role + /// + /// + public class TestUserRole where TKey : IEquatable + { + /// + /// UserId for the user that is in the role + /// + public virtual TKey UserId { get; set; } + + /// + /// RoleId for the role + /// + public virtual TKey RoleId { get; set; } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestUserToken.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestUserToken.cs new file mode 100644 index 0000000..2df5fa5 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Shared/TestUserToken.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Identity.Test +{ + /// + /// Entity type for a user's token + /// + public class TestUserToken : TestUserToken { } + + /// + /// Entity type for a user's token + /// + /// + public class TestUserToken where TKey : IEquatable + { + /// + /// The login provider for the login (i.e. facebook, google) + /// + public virtual string LoginProvider { get; set; } + + /// + /// Key representing the login for the provider + /// + public virtual string TokenName { get; set; } + + /// + /// Display name for the login + /// + public virtual string TokenValue { get; set; } + + /// + /// User Id for the user who owns this login + /// + public virtual TKey UserId { get; set; } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Specification/IdentityResultAssert.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Specification/IdentityResultAssert.cs new file mode 100644 index 0000000..4246d17 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Specification/IdentityResultAssert.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Test +{ + /// + /// Helper for tests to validate identity results. + /// + public static class IdentityResultAssert + { + /// + /// Asserts that the result has Succeeded. + /// + /// + public static void IsSuccess(IdentityResult result) + { + Assert.NotNull(result); + Assert.True(result.Succeeded); + } + + /// + /// Asserts that the result has not Succeeded. + /// + public static void IsFailure(IdentityResult result) + { + Assert.NotNull(result); + Assert.False(result.Succeeded); + } + + /// + /// Asserts that the result has not Succeeded and that error is the first Error's Description. + /// + public static void IsFailure(IdentityResult result, string error) + { + Assert.NotNull(result); + Assert.False(result.Succeeded); + Assert.Equal(error, result.Errors.First().Description); + } + + /// + /// Asserts that the result has not Succeeded and that first error matches error's code and Description. + /// + public static void IsFailure(IdentityResult result, IdentityError error) + { + Assert.NotNull(result); + Assert.False(result.Succeeded); + Assert.Equal(error.Description, result.Errors.First().Description); + Assert.Equal(error.Code, result.Errors.First().Code); + } + + /// + /// Asserts that the logger contains the expectedLog. + /// + /// The logger to inspect. + /// The expected log message. + public static void VerifyLogMessage(ILogger logger, string expectedLog) + { + var testlogger = logger as ITestLogger; + if (testlogger != null) + { + Assert.Contains(expectedLog, testlogger.LogMessages); + } + else + { + Assert.False(true, "No logger registered"); + } + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Specification/IdentitySpecificationTestBase.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Specification/IdentitySpecificationTestBase.cs new file mode 100644 index 0000000..f50abb4 --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Specification/IdentitySpecificationTestBase.cs @@ -0,0 +1,717 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; +using AspNetCore.Identity.MongoDbCore.Models; +using AspNetCore.Identity.MongoDbCore.Extensions; +using AspNetCore.Identity.MongoDbCore.Infrastructure; +using AspNetCore.Identity.MongoDbCore.IntegrationTests.Infrastructure; + +namespace Microsoft.AspNetCore.Identity.Test +{ + /// + /// Common functionality tests that all verifies user manager functionality regardless of store implementation + /// + /// The type of the user. + /// The type of the role. + public abstract class IdentitySpecificationTestBase : IdentitySpecificationTestBase + where TUser : MongoDbIdentityUser, new() + where TRole : MongoDbIdentityRole, new() + { } + + /// + /// Base class for tests that exercise basic identity functionality that all stores should support. + /// + /// The type of the user. + /// The type of the role. + /// The primary key type. + public abstract class IdentitySpecificationTestBase : UserManagerSpecificationTestBase + where TUser : MongoIdentityUser, new() + where TRole : MongoIdentityRole, new() + where TKey : IEquatable + { + + /// + /// Configure the service collection used for tests. + /// + /// + /// + protected override void SetupIdentityServices(IServiceCollection services) + { + services.AddSingleton(); + services.ConfigureMongoDbIdentity(Container.MongoDbIdentityConfiguration, Container.MongoRepository.Context); + services.AddLogging(); + services.AddSingleton>>(new TestLogger>()); + services.AddSingleton>>(new TestLogger>()); + } + + /// + /// Configure the service collection used for tests. + /// + /// + /// + protected void SetupIdentityServices(IServiceCollection services, bool concurrentSetup = false) + { + services.AddSingleton(); + if (concurrentSetup) + { + services.ConfigureMongoDbIdentity(Container.MongoDbIdentityConfiguration, Container.MongoRepositoryConcurrent.Context); + } + else + { + services.ConfigureMongoDbIdentity(Container.MongoDbIdentityConfiguration, Container.MongoRepository.Context); + } + + services.AddLogging(); + services.AddSingleton>>(new TestLogger>()); + services.AddSingleton>>(new TestLogger>()); + } + + /// + /// Setup the IdentityBuilder + /// + /// + /// + /// + protected override IServiceCollection SetupBuilder(IServiceCollection services) + { + var builder = base.SetupBuilder(services); + //builder.AddRoles(); + //AddRoleStore(services, context); + services.AddSingleton>>(new TestLogger>()); + return builder; + } + + /// + /// Creates the role manager for tests. + /// + /// The context that will be passed into the store, typically a db context. + /// The service collection to use, optional. + /// + protected virtual RoleManager CreateRoleManager(object context = null, IServiceCollection services = null) + { + if (services == null) + { + services = new ServiceCollection(); + } + + if(context == null) + { + SetupIdentityServices(services); + } + else + { + SetupIdentityServices(services, true); + } + + return services.BuildServiceProvider().GetService>(); + } + + /// + /// Adds an IRoleStore to services for the test. + /// + /// The service collection to add to. + /// The context for the store to use, optional. + protected abstract void AddRoleStore(IServiceCollection services, object context = null); + + /// + /// Creates a new test role instance. + /// + /// Optional name prefix, name will be randomized. + /// If true, the prefix should be used as the rolename without a random pad. + /// + protected abstract TRole CreateTestRole(string roleNamePrefix = "", bool useRoleNamePrefixAsRoleName = false); + + /// + /// Query used to do name equality checks. + /// + /// The role name to match. + /// The query to use. + protected abstract Expression> RoleNameEqualsPredicate(string roleName); + + /// + /// Query used to do user name prefix matching. + /// + /// The role name to match. + /// The query to use. + protected abstract Expression> RoleNameStartsWithPredicate(string roleName); + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanCreateRoleTest() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateRoleManager(); + var roleName = "create" + Guid.NewGuid().ToString(); + var role = CreateTestRole(roleName, useRoleNamePrefixAsRoleName: true); + Assert.False(await manager.RoleExistsAsync(roleName)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + Assert.True(await manager.RoleExistsAsync(roleName)); + } + + private class AlwaysBadValidator : IUserValidator, IRoleValidator, + IPasswordValidator + { + public static readonly IdentityError ErrorMessage = new IdentityError { Description = "I'm Bad.", Code = "BadValidator" }; + + public Task ValidateAsync(UserManager manager, TUser user, string password) + { + return Task.FromResult(IdentityResult.Failed(ErrorMessage)); + } + + public Task ValidateAsync(RoleManager manager, TRole role) + { + return Task.FromResult(IdentityResult.Failed(ErrorMessage)); + } + + public Task ValidateAsync(UserManager manager, TUser user) + { + return Task.FromResult(IdentityResult.Failed(ErrorMessage)); + } + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task BadValidatorBlocksCreateRole() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateRoleManager(); + manager.RoleValidators.Clear(); + manager.RoleValidators.Add(new AlwaysBadValidator()); + var role = CreateTestRole("blocked"); + IdentityResultAssert.IsFailure(await manager.CreateAsync(role), + AlwaysBadValidator.ErrorMessage); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"Role {await manager.GetRoleIdAsync(role) ?? NullValue} validation failed: {AlwaysBadValidator.ErrorMessage.Code}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanChainRoleValidators() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateRoleManager(); + manager.RoleValidators.Clear(); + manager.RoleValidators.Add(new AlwaysBadValidator()); + manager.RoleValidators.Add(new AlwaysBadValidator()); + var role = CreateTestRole("blocked"); + var result = await manager.CreateAsync(role); + IdentityResultAssert.IsFailure(result, AlwaysBadValidator.ErrorMessage); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"Role {await manager.GetRoleIdAsync(role) ?? NullValue} validation failed: {AlwaysBadValidator.ErrorMessage.Code};{AlwaysBadValidator.ErrorMessage.Code}."); + Assert.Equal(2, result.Errors.Count()); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task BadValidatorBlocksRoleUpdate() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateRoleManager(); + var role = CreateTestRole("poorguy"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + var error = AlwaysBadValidator.ErrorMessage; + manager.RoleValidators.Clear(); + manager.RoleValidators.Add(new AlwaysBadValidator()); + IdentityResultAssert.IsFailure(await manager.UpdateAsync(role), error); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"Role {await manager.GetRoleIdAsync(role) ?? NullValue} validation failed: {AlwaysBadValidator.ErrorMessage.Code}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanDeleteRole() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateRoleManager(); + var roleName = "delete" + Guid.NewGuid().ToString(); + var role = CreateTestRole(roleName, useRoleNamePrefixAsRoleName: true); + Assert.False(await manager.RoleExistsAsync(roleName)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + Assert.True(await manager.RoleExistsAsync(roleName)); + IdentityResultAssert.IsSuccess(await manager.DeleteAsync(role)); + Assert.False(await manager.RoleExistsAsync(roleName)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanAddRemoveRoleClaim() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateRoleManager(); + var role = CreateTestRole("ClaimsAddRemove"); + var roleSafe = CreateTestRole("ClaimsAdd"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(roleSafe)); + Claim[] claims = { new Claim("c", "v"), new Claim("c2", "v2"), new Claim("c2", "v3") }; + foreach (Claim c in claims) + { + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(role, c)); + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(roleSafe, c)); + } + var roleClaims = await manager.GetClaimsAsync(role); + var safeRoleClaims = await manager.GetClaimsAsync(roleSafe); + Assert.Equal(3, roleClaims.Count); + Assert.Equal(3, safeRoleClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(role, claims[0])); + roleClaims = await manager.GetClaimsAsync(role); + safeRoleClaims = await manager.GetClaimsAsync(roleSafe); + Assert.Equal(2, roleClaims.Count); + Assert.Equal(3, safeRoleClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(role, claims[1])); + roleClaims = await manager.GetClaimsAsync(role); + safeRoleClaims = await manager.GetClaimsAsync(roleSafe); + Assert.Equal(1, roleClaims.Count); + Assert.Equal(3, safeRoleClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(role, claims[2])); + roleClaims = await manager.GetClaimsAsync(role); + safeRoleClaims = await manager.GetClaimsAsync(roleSafe); + Assert.Equal(0, roleClaims.Count); + Assert.Equal(3, safeRoleClaims.Count); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanRoleFindById() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateRoleManager(); + var role = CreateTestRole("FindByIdAsync"); + Assert.Null(await manager.FindByIdAsync(await manager.GetRoleIdAsync(role))); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + Assert.Equal(role.Id, (await manager.FindByIdAsync(await manager.GetRoleIdAsync(role))).Id); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanRoleFindByName() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateRoleManager(); + var roleName = "FindByNameAsync" + Guid.NewGuid().ToString(); + var role = CreateTestRole(roleName, useRoleNamePrefixAsRoleName: true); + Assert.Null(await manager.FindByNameAsync(roleName)); + Assert.False(await manager.RoleExistsAsync(roleName)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + Assert.Equal(role.Id, (await manager.FindByNameAsync(roleName)).Id); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanUpdateRoleName() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateRoleManager(); + var roleName = "update" + Guid.NewGuid().ToString(); + var changedRoleName = "Changed" + Guid.NewGuid().ToString(); + var role = CreateTestRole(roleName, useRoleNamePrefixAsRoleName: true); + Assert.False(await manager.RoleExistsAsync(roleName)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + Assert.True(await manager.RoleExistsAsync(roleName)); + IdentityResultAssert.IsSuccess(await manager.SetRoleNameAsync(role, changedRoleName)); + IdentityResultAssert.IsSuccess(await manager.UpdateAsync(role)); + Assert.False(await manager.RoleExistsAsync(roleName)); + Assert.Equal(role.Id, (await manager.FindByNameAsync(changedRoleName)).Id); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanQueryableRoles() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateRoleManager(); + if (manager.SupportsQueryableRoles) + { + var guidName = "CanQuerableRolesTest" + Guid.NewGuid().ToString("n"); + var roles = GenerateRoles(guidName, 4); + foreach (var r in roles) + { + IdentityResultAssert.IsSuccess(await manager.CreateAsync(r)); + } + Expression> func = RoleNameStartsWithPredicate(guidName); + Assert.Equal(roles.Count, manager.Roles.Count(func)); + func = RoleNameEqualsPredicate("bogus"); + Assert.Null(manager.Roles.FirstOrDefault(func)); + + } + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CreateRoleFailsIfExists() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateRoleManager(); + var roleName = "dupeRole" + Guid.NewGuid().ToString(); + var role = CreateTestRole(roleName, useRoleNamePrefixAsRoleName: true); + Assert.False(await manager.RoleExistsAsync(roleName)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(role)); + Assert.True(await manager.RoleExistsAsync(roleName)); + var role2 = CreateTestRole(roleName, useRoleNamePrefixAsRoleName: true); + IdentityResultAssert.IsFailure(await manager.CreateAsync(role2)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanAddUsersToRole() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var roleManager = CreateRoleManager(); + var roleName = "AddUserTest" + Guid.NewGuid().ToString(); + var role = CreateTestRole(roleName, useRoleNamePrefixAsRoleName: true); + IdentityResultAssert.IsSuccess(await roleManager.CreateAsync(role)); + TUser[] users = + { + CreateTestUser("1"),CreateTestUser("2"),CreateTestUser("3"),CreateTestUser("4"), + }; + foreach (var u in users) + { + IdentityResultAssert.IsSuccess(await manager.CreateAsync(u)); + IdentityResultAssert.IsSuccess(await manager.AddToRoleAsync(u, roleName)); + Assert.True(await manager.IsInRoleAsync(u, roleName)); + } + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanGetRolesForUser() + { + if (ShouldSkipDbTests()) + { + return; + } + + var userManager = CreateManager(); + var roleManager = CreateRoleManager(); + var guid = Guid.NewGuid().ToString("n"); + var users = GenerateUsers(guid + "CanGetRolesForUser", 4); + var roles = GenerateRoles(guid + "CanGetRolesForUserRole", 4); + foreach (var u in users) + { + IdentityResultAssert.IsSuccess(await userManager.CreateAsync(u)); + } + foreach (var r in roles) + { + IdentityResultAssert.IsSuccess(await roleManager.CreateAsync(r)); + foreach (var u in users) + { + IdentityResultAssert.IsSuccess(await userManager.AddToRoleAsync(u, await roleManager.GetRoleNameAsync(r))); + Assert.True(await userManager.IsInRoleAsync(u, await roleManager.GetRoleNameAsync(r))); + } + } + + foreach (var u in users) + { + var rs = await userManager.GetRolesAsync(u); + Assert.Equal(roles.Count, rs.Count); + foreach (var r in roles) + { + var expectedRoleName = await roleManager.GetRoleNameAsync(r); + Assert.True(rs.Any(role => role == expectedRoleName)); + } + } + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task RemoveUserFromRoleWithMultipleRoles() + { + if (ShouldSkipDbTests()) + { + return; + } + var userManager = CreateManager(); + var roleManager = CreateRoleManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await userManager.CreateAsync(user)); + var guidName = "RemoveUserFromRoleWithMultipleRoles" + Guid.NewGuid().ToString("n"); + var roles = GenerateRoles(guidName, 4); + foreach (var r in roles) + { + IdentityResultAssert.IsSuccess(await roleManager.CreateAsync(r)); + IdentityResultAssert.IsSuccess(await userManager.AddToRoleAsync(user, await roleManager.GetRoleNameAsync(r))); + Assert.True(await userManager.IsInRoleAsync(user, await roleManager.GetRoleNameAsync(r))); + } + IdentityResultAssert.IsSuccess(await userManager.RemoveFromRoleAsync(user, await roleManager.GetRoleNameAsync(roles[2]))); + Assert.False(await userManager.IsInRoleAsync(user, await roleManager.GetRoleNameAsync(roles[2]))); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanRemoveUsersFromRole() + { + if (ShouldSkipDbTests()) + { + return; + } + var userManager = CreateManager(); + var roleManager = CreateRoleManager(); + var guid = Guid.NewGuid().ToString("n"); + var users = GenerateUsers(guid + "CanRemoveUsersFromRole", 4); + foreach (var u in users) + { + IdentityResultAssert.IsSuccess(await userManager.CreateAsync(u)); + } + var r = CreateTestRole(guid + "r1"); + var roleName = await roleManager.GetRoleNameAsync(r); + IdentityResultAssert.IsSuccess(await roleManager.CreateAsync(r)); + foreach (var u in users) + { + IdentityResultAssert.IsSuccess(await userManager.AddToRoleAsync(u, roleName)); + Assert.True(await userManager.IsInRoleAsync(u, roleName)); + } + foreach (var u in users) + { + IdentityResultAssert.IsSuccess(await userManager.RemoveFromRoleAsync(u, roleName)); + Assert.False(await userManager.IsInRoleAsync(u, roleName)); + } + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task RemoveUserNotInRoleFails() + { + if (ShouldSkipDbTests()) + { + return; + } + var userMgr = CreateManager(); + var roleMgr = CreateRoleManager(); + var roleName = "addUserDupeTest" + Guid.NewGuid().ToString(); + var role = CreateTestRole(roleName, useRoleNamePrefixAsRoleName: true); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await userMgr.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await roleMgr.CreateAsync(role)); + var result = await userMgr.RemoveFromRoleAsync(user, roleName); + IdentityResultAssert.IsFailure(result, _errorDescriber.UserNotInRole(roleName)); + IdentityResultAssert.VerifyLogMessage(userMgr.Logger, $"User {await userMgr.GetUserIdAsync(user)} is not in role {roleName}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task AddUserToRoleFailsIfAlreadyInRole() + { + if (ShouldSkipDbTests()) + { + return; + } + var userMgr = CreateManager(); + var roleMgr = CreateRoleManager(); + var roleName = "addUserDupeTest" + Guid.NewGuid().ToString(); + var role = CreateTestRole(roleName, useRoleNamePrefixAsRoleName: true); + + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await userMgr.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await roleMgr.CreateAsync(role)); + IdentityResultAssert.IsSuccess(await userMgr.AddToRoleAsync(user, roleName)); + Assert.True(await userMgr.IsInRoleAsync(user, roleName)); + IdentityResultAssert.IsFailure(await userMgr.AddToRoleAsync(user, roleName), _errorDescriber.UserAlreadyInRole(roleName)); + IdentityResultAssert.VerifyLogMessage(userMgr.Logger, $"User {await userMgr.GetUserIdAsync(user)} is already in role {roleName}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task AddUserToRolesIgnoresDuplicates() + { + if (ShouldSkipDbTests()) + { + return; + } + var userMgr = CreateManager(); + var roleMgr = CreateRoleManager(); + var roleName = "addUserDupeTest" + Guid.NewGuid().ToString(); + var role = CreateTestRole(roleName, useRoleNamePrefixAsRoleName: true); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await userMgr.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await roleMgr.CreateAsync(role)); + Assert.False(await userMgr.IsInRoleAsync(user, roleName)); + IdentityResultAssert.IsSuccess(await userMgr.AddToRolesAsync(user, new[] { roleName, roleName })); + Assert.True(await userMgr.IsInRoleAsync(user, roleName)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanFindRoleByNameWithManager() + { + if (ShouldSkipDbTests()) + { + return; + } + var roleMgr = CreateRoleManager(); + var roleName = "findRoleByNameTest" + Guid.NewGuid().ToString(); + var role = CreateTestRole(roleName, useRoleNamePrefixAsRoleName: true); + IdentityResultAssert.IsSuccess(await roleMgr.CreateAsync(role)); + Assert.NotNull(await roleMgr.FindByNameAsync(roleName)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanFindRoleWithManager() + { + if (ShouldSkipDbTests()) + { + return; + } + var roleMgr = CreateRoleManager(); + var roleName = "findRoleTest" + Guid.NewGuid().ToString(); + var role = CreateTestRole(roleName, useRoleNamePrefixAsRoleName: true); + IdentityResultAssert.IsSuccess(await roleMgr.CreateAsync(role)); + Assert.Equal(roleName, await roleMgr.GetRoleNameAsync(await roleMgr.FindByNameAsync(roleName))); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanGetUsersInRole() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var roleManager = CreateRoleManager(); + var guidName = Guid.NewGuid().ToString("n") + "UsersInRole"; + var roles = GenerateRoles(guidName, 4); + var roleNameList = new List(); + + foreach (var role in roles) + { + IdentityResultAssert.IsSuccess(await roleManager.CreateAsync(role)); + roleNameList.Add(await roleManager.GetRoleNameAsync(role)); + } + + for (int i = 0; i < 6; i++) + { + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + + if ((i % 2) == 0) + { + IdentityResultAssert.IsSuccess(await manager.AddToRolesAsync(user, roleNameList)); + } + } + + foreach (var role in roles) + { + Assert.Equal(3, (await manager.GetUsersInRoleAsync(await roleManager.GetRoleNameAsync(role))).Count); + } + + Assert.Equal(0, (await manager.GetUsersInRoleAsync("123456")).Count); + } + + private List GenerateRoles(string namePrefix, int count) + { + var roles = new List(count); + for (var i = 0; i < count; i++) + { + roles.Add(CreateTestRole(namePrefix + i)); + } + return roles; + } + } +} diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Specification/TestLogger.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Specification/TestLogger.cs new file mode 100644 index 0000000..7b9ebde --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Specification/TestLogger.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Identity.Test +{ + /// + /// test logger. + /// + public interface ITestLogger + { + /// + /// log messages. + /// + IList LogMessages { get; } + } + + /// + /// Test logger. + /// + /// + public class TestLogger : ILogger, ITestLogger + { + /// + /// log messages. + /// + public IList LogMessages { get; } = new List(); + + /// + /// + /// + /// + /// + public IDisposable BeginScope(TState state) + { + LogMessages.Add(state?.ToString()); + return null; + } + + /// + /// + /// + /// + /// + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (formatter == null) + { + LogMessages.Add(state.ToString()); + } + else + { + LogMessages.Add(formatter(state, exception)); + } + } + } +} \ No newline at end of file diff --git a/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Specification/UserManagerSpecificationTests.cs b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Specification/UserManagerSpecificationTests.cs new file mode 100644 index 0000000..cec91fa --- /dev/null +++ b/test/AspNetCore.Identity.MongoDbCore.IntegrationTests/Specification/UserManagerSpecificationTests.cs @@ -0,0 +1,2116 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; +using AspNetCore.Identity.MongoDbCore.Extensions; +using AspNetCore.Identity.MongoDbCore.IntegrationTests.Infrastructure; +using AspNetCore.Identity.MongoDbCore.Models; +using AspNetCore.Identity.MongoDbCore.Infrastructure; +using MongoDbGenericRepository; +using AspNetCore.Identity.MongoDbCore; + +namespace Microsoft.AspNetCore.Identity.Test +{ + /// + /// Base class for tests that exercise basic identity functionality that all stores should support. + /// + /// The type of the user. + public abstract class UserManagerSpecificationTestBase : UserManagerSpecificationTestBase where TUser : MongoDbIdentityUser, new() + { + } + + /// + /// Base class for tests that exercise basic identity functionality that all stores should support. + /// + /// The type of the user. + /// The primary key type. + public abstract class UserManagerSpecificationTestBase + where TUser : MongoIdentityUser, new() + where TKey : IEquatable + { + + /// + /// Null value. + /// + protected const string NullValue = "(null)"; + + /// + /// Error describer. + /// + protected readonly IdentityErrorDescriber _errorDescriber = new IdentityErrorDescriber(); + + /// + /// Configure the service collection used for tests. + /// + /// + /// + protected virtual void SetupIdentityServices(IServiceCollection services) + => SetupBuilder(services); + + /// + /// Configure the service collection used for tests. + /// + /// + /// + protected virtual IServiceCollection SetupBuilder(IServiceCollection services) + { + services.AddSingleton(); + services.AddLogging(); + + services.AddSingleton>(provider => + { + var userStore = new MongoUserStore, IMongoDbContext, TKey>(provider.GetService().Context); + return userStore; + }); + services.AddSingleton>>(new TestLogger>()); + return services; + } + + /// + /// If true, tests that require a database will be skipped. + /// + /// + protected virtual bool ShouldSkipDbTests() => false; + + /// + /// Creates the user manager used for tests. + /// + /// The context that will be passed into the store, typically a db context. + /// The service collection to use, optional. + /// Delegate used to configure the services, optional. + /// The user manager to use for tests. + protected virtual UserManager CreateManager(object context = null, IServiceCollection services = null, Action configureServices = null) + { + if (services == null) + { + services = new ServiceCollection(); + } + SetupIdentityServices(services); + configureServices?.Invoke(services); + return services.BuildServiceProvider().GetService>(); + } + + ///// + ///// Creates the context object for a test, typically a DbContext. + ///// + ///// The context object for a test, typically a DbContext. + //protected abstract object CreateTestContext(); + + /// + /// Adds an IUserStore to services for the test. + /// + /// The service collection to add to. + /// The context for the store to use, optional. + protected abstract void AddUserStore(IServiceCollection services, object context = null); + + /// + /// Set the user's password hash. + /// + /// The user to set. + /// The password hash to set. + protected abstract void SetUserPasswordHash(TUser user, string hashedPassword); + + /// + /// Create a new test user instance. + /// + /// Optional name prefix, name will be randomized. + /// Optional email. + /// Optional phone number. + /// Optional lockout enabled. + /// Optional lockout end. + /// If true, the prefix should be used as the username without a random pad. + /// The new test user instance. + protected abstract TUser CreateTestUser(string namePrefix = "", string email = "", string phoneNumber = "", + bool lockoutEnabled = false, DateTimeOffset? lockoutEnd = null, bool useNamePrefixAsUserName = false); + + /// + /// Query used to do name equality checks. + /// + /// The user name to match. + /// The query to use. + protected abstract Expression> UserNameEqualsPredicate(string userName); + + /// + /// Query used to do user name prefix matching. + /// + /// The user name to match. + /// The query to use. + protected abstract Expression> UserNameStartsWithPredicate(string userName); + + private class AlwaysBadValidator : IUserValidator, + IPasswordValidator + { + public static readonly IdentityError ErrorMessage = new IdentityError { Description = "I'm Bad.", Code = "BadValidator" }; + + public Task ValidateAsync(UserManager manager, TUser user, string password) + { + return Task.FromResult(IdentityResult.Failed(ErrorMessage)); + } + + public Task ValidateAsync(UserManager manager, TUser user) + { + return Task.FromResult(IdentityResult.Failed(ErrorMessage)); + } + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CreateUserWillSetCreateDateOnlyIfSupported() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var userId = await manager.GetUserIdAsync(user); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanDeleteUser() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var userId = await manager.GetUserIdAsync(user); + IdentityResultAssert.IsSuccess(await manager.DeleteAsync(user)); + Assert.Null(await manager.FindByIdAsync(userId)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanUpdateUserName() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var name = Guid.NewGuid().ToString(); + var user = CreateTestUser(name); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var newName = Guid.NewGuid().ToString(); + Assert.Null(await manager.FindByNameAsync(newName)); + IdentityResultAssert.IsSuccess(await manager.SetUserNameAsync(user, newName)); + IdentityResultAssert.IsSuccess(await manager.UpdateAsync(user)); + Assert.NotNull(await manager.FindByNameAsync(newName)); + Assert.Null(await manager.FindByNameAsync(name)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CheckSetUserNameValidatesUser() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var username = "UpdateAsync" + Guid.NewGuid().ToString(); + var newUsername = "New" + Guid.NewGuid().ToString(); + var user = CreateTestUser(username, useNamePrefixAsUserName: true); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + Assert.Null(await manager.FindByNameAsync(newUsername)); + IdentityResultAssert.IsSuccess(await manager.SetUserNameAsync(user, newUsername)); + Assert.NotNull(await manager.FindByNameAsync(newUsername)); + Assert.Null(await manager.FindByNameAsync(username)); + + var newUser = CreateTestUser(username, useNamePrefixAsUserName: true); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(newUser)); + var error = _errorDescriber.InvalidUserName(""); + IdentityResultAssert.IsFailure(await manager.SetUserNameAsync(newUser, ""), error); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"User {await manager.GetUserIdAsync(newUser)} validation failed: {error.Code}."); + + error = _errorDescriber.DuplicateUserName(newUsername); + IdentityResultAssert.IsFailure(await manager.SetUserNameAsync(newUser, newUsername), error); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"User {await manager.GetUserIdAsync(newUser)} validation failed: {error.Code}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task SetUserNameUpdatesSecurityStamp() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var username = "UpdateAsync" + Guid.NewGuid().ToString(); + var newUsername = "New" + Guid.NewGuid().ToString(); + var user = CreateTestUser(username, useNamePrefixAsUserName: true); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var stamp = await manager.GetSecurityStampAsync(user); + Assert.Null(await manager.FindByNameAsync(newUsername)); + IdentityResultAssert.IsSuccess(await manager.SetUserNameAsync(user, newUsername)); + Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CreateUpdatesSecurityStamp() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var username = "Create" + Guid.NewGuid().ToString(); + var user = CreateTestUser(username, useNamePrefixAsUserName: true); + var stamp = await manager.GetSecurityStampAsync(user); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + Assert.NotNull(await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CheckSetEmailValidatesUser() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + manager.Options.User.RequireUniqueEmail = true; + manager.UserValidators.Add(new UserValidator()); + var random = new Random(); + var email = "foo" + random.Next() + "@example.com"; + var newEmail = "bar" + random.Next() + "@example.com"; + var user = CreateTestUser(email: email); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, newEmail)); + + var newUser = CreateTestUser(email: email); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(newUser)); + IdentityResultAssert.IsFailure(await manager.SetEmailAsync(newUser, newEmail), _errorDescriber.DuplicateEmail(newEmail)); + IdentityResultAssert.IsFailure(await manager.SetEmailAsync(newUser, ""), _errorDescriber.InvalidEmail("")); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanUpdatePasswordUsingHasher() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser("UpdatePassword"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, "password")); + Assert.True(await manager.CheckPasswordAsync(user, "password")); + var userId = await manager.GetUserIdAsync(user); + + SetUserPasswordHash(user, manager.PasswordHasher.HashPassword(user, "New")); + IdentityResultAssert.IsSuccess(await manager.UpdateAsync(user)); + Assert.False(await manager.CheckPasswordAsync(user, "password")); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"Invalid password for user {await manager.GetUserIdAsync(user)}."); + Assert.True(await manager.CheckPasswordAsync(user, "New")); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanFindById() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + Assert.NotNull(await manager.FindByIdAsync(await manager.GetUserIdAsync(user))); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task UserValidatorCanBlockCreate() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + manager.UserValidators.Clear(); + manager.UserValidators.Add(new AlwaysBadValidator()); + IdentityResultAssert.IsFailure(await manager.CreateAsync(user), AlwaysBadValidator.ErrorMessage); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"User {await manager.GetUserIdAsync(user) ?? NullValue} validation failed: {AlwaysBadValidator.ErrorMessage.Code}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task UserValidatorCanBlockUpdate() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + manager.UserValidators.Clear(); + manager.UserValidators.Add(new AlwaysBadValidator()); + IdentityResultAssert.IsFailure(await manager.UpdateAsync(user), AlwaysBadValidator.ErrorMessage); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"User {await manager.GetUserIdAsync(user) ?? NullValue} validation failed: {AlwaysBadValidator.ErrorMessage.Code}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanChainUserValidators() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + manager.UserValidators.Clear(); + var user = CreateTestUser(); + manager.UserValidators.Add(new AlwaysBadValidator()); + manager.UserValidators.Add(new AlwaysBadValidator()); + var result = await manager.CreateAsync(user); + IdentityResultAssert.IsFailure(result, AlwaysBadValidator.ErrorMessage); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"User {await manager.GetUserIdAsync(user) ?? NullValue} validation failed: {AlwaysBadValidator.ErrorMessage.Code};{AlwaysBadValidator.ErrorMessage.Code}."); + Assert.Equal(2, result.Errors.Count()); + } + + /// + /// Test. + /// + /// Task + [Theory] + [InlineData("")] + [InlineData(null)] + public async Task UserValidatorBlocksShortEmailsWhenRequiresUniqueEmail(string email) + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + manager.Options.User.RequireUniqueEmail = true; + IdentityResultAssert.IsFailure(await manager.CreateAsync(user), _errorDescriber.InvalidEmail(email)); + } + + /// + /// Test. + /// + /// Task + [Theory] + [InlineData("@@afd")] + [InlineData("bogus")] + public async Task UserValidatorBlocksInvalidEmailsWhenRequiresUniqueEmail(string email) + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser("UpdateBlocked", email); + manager.Options.User.RequireUniqueEmail = true; + IdentityResultAssert.IsFailure(await manager.CreateAsync(user), _errorDescriber.InvalidEmail(email)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task PasswordValidatorCanBlockAddPassword() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + manager.PasswordValidators.Clear(); + manager.PasswordValidators.Add(new AlwaysBadValidator()); + IdentityResultAssert.IsFailure(await manager.AddPasswordAsync(user, "password"), + AlwaysBadValidator.ErrorMessage); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"User {await manager.GetUserIdAsync(user)} password validation failed: {AlwaysBadValidator.ErrorMessage.Code}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanChainPasswordValidators() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + manager.PasswordValidators.Clear(); + manager.PasswordValidators.Add(new AlwaysBadValidator()); + manager.PasswordValidators.Add(new AlwaysBadValidator()); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var result = await manager.AddPasswordAsync(user, "pwd"); + IdentityResultAssert.IsFailure(result, AlwaysBadValidator.ErrorMessage); + Assert.Equal(2, result.Errors.Count()); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task PasswordValidatorCanBlockChangePassword() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, "password")); + manager.PasswordValidators.Clear(); + manager.PasswordValidators.Add(new AlwaysBadValidator()); + IdentityResultAssert.IsFailure(await manager.ChangePasswordAsync(user, "password", "new"), + AlwaysBadValidator.ErrorMessage); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"User {await manager.GetUserIdAsync(user) ?? NullValue} password validation failed: {AlwaysBadValidator.ErrorMessage.Code}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task PasswordValidatorCanBlockCreateUser() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + manager.PasswordValidators.Clear(); + manager.PasswordValidators.Add(new AlwaysBadValidator()); + IdentityResultAssert.IsFailure(await manager.CreateAsync(user, "password"), AlwaysBadValidator.ErrorMessage); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"User {await manager.GetUserIdAsync(user) ?? NullValue} password validation failed: {AlwaysBadValidator.ErrorMessage.Code}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanCreateUserNoPassword() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var username = "CreateUserTest" + Guid.NewGuid(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(CreateTestUser(username, useNamePrefixAsUserName: true))); + var user = await manager.FindByNameAsync(username); + Assert.NotNull(user); + Assert.False(await manager.HasPasswordAsync(user)); + Assert.False(await manager.CheckPasswordAsync(user, "whatever")); + var logins = await manager.GetLoginsAsync(user); + Assert.NotNull(logins); + Assert.Empty(logins); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanCreateUserAddLogin() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + const string provider = "ZzAuth"; + const string display = "display"; + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var providerKey = await manager.GetUserIdAsync(user); + IdentityResultAssert.IsSuccess(await manager.AddLoginAsync(user, new UserLoginInfo(provider, providerKey, display))); + var logins = await manager.GetLoginsAsync(user); + Assert.NotNull(logins); + Assert.Single(logins); + Assert.Equal(provider, logins.First().LoginProvider); + Assert.Equal(providerKey, logins.First().ProviderKey); + Assert.Equal(display, logins.First().ProviderDisplayName); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanCreateUserLoginAndAddPassword() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var userId = await manager.GetUserIdAsync(user); + var login = new UserLoginInfo("Provider", userId, "display"); + IdentityResultAssert.IsSuccess(await manager.AddLoginAsync(user, login)); + Assert.False(await manager.HasPasswordAsync(user)); + IdentityResultAssert.IsSuccess(await manager.AddPasswordAsync(user, "password")); + Assert.True(await manager.HasPasswordAsync(user)); + var logins = await manager.GetLoginsAsync(user); + Assert.NotNull(logins); + Assert.Single(logins); + Assert.Equal(user.Id, (await manager.FindByLoginAsync(login.LoginProvider, login.ProviderKey)).Id); + Assert.True(await manager.CheckPasswordAsync(user, "password")); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task AddPasswordFailsIfAlreadyHave() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, "Password")); + Assert.True(await manager.HasPasswordAsync(user)); + IdentityResultAssert.IsFailure(await manager.AddPasswordAsync(user, "password"), + "User already has a password set."); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"User {await manager.GetUserIdAsync(user)} already has a password."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanCreateUserAddRemoveLogin() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + var result = await manager.CreateAsync(user); + Assert.NotNull(user); + var userId = await manager.GetUserIdAsync(user); + var login = new UserLoginInfo("Provider", userId, "display"); + IdentityResultAssert.IsSuccess(result); + IdentityResultAssert.IsSuccess(await manager.AddLoginAsync(user, login)); + Assert.Equal(user.Id, (await manager.FindByLoginAsync(login.LoginProvider, login.ProviderKey)).Id); + var logins = await manager.GetLoginsAsync(user); + Assert.NotNull(logins); + Assert.Single(logins); + Assert.Equal(login.LoginProvider, logins.Last().LoginProvider); + Assert.Equal(login.ProviderKey, logins.Last().ProviderKey); + Assert.Equal(login.ProviderDisplayName, logins.Last().ProviderDisplayName); + var stamp = await manager.GetSecurityStampAsync(user); + IdentityResultAssert.IsSuccess(await manager.RemoveLoginAsync(user, login.LoginProvider, login.ProviderKey)); + Assert.Null(await manager.FindByLoginAsync(login.LoginProvider, login.ProviderKey)); + logins = await manager.GetLoginsAsync(user); + Assert.NotNull(logins); + Assert.Empty(logins); + Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanRemovePassword() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser("CanRemovePassword"); + const string password = "password"; + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, password)); + var stamp = await manager.GetSecurityStampAsync(user); + var username = await manager.GetUserNameAsync(user); + IdentityResultAssert.IsSuccess(await manager.RemovePasswordAsync(user)); + var u = await manager.FindByNameAsync(username); + Assert.NotNull(u); + Assert.False(await manager.HasPasswordAsync(user)); + Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanChangePassword() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + const string password = "password"; + const string newPassword = "newpassword"; + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, password)); + var stamp = await manager.GetSecurityStampAsync(user); + Assert.NotNull(stamp); + IdentityResultAssert.IsSuccess(await manager.ChangePasswordAsync(user, password, newPassword)); + Assert.False(await manager.CheckPasswordAsync(user, password)); + Assert.True(await manager.CheckPasswordAsync(user, newPassword)); + Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanAddRemoveUserClaim() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + Claim[] claims = { new Claim("c", "v"), new Claim("c2", "v2"), new Claim("c2", "v3") }; + foreach (Claim c in claims) + { + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, c)); + } + var userId = await manager.GetUserIdAsync(user); + var userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(3, userClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[0])); + userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(2, userClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[1])); + userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(1, userClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[2])); + userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(0, userClaims.Count); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task RemoveClaimOnlyAffectsUser() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + var user2 = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user2)); + Claim[] claims = { new Claim("c", "v"), new Claim("c2", "v2"), new Claim("c2", "v3") }; + foreach (Claim c in claims) + { + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, c)); + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user2, c)); + } + var userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(3, userClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[0])); + userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(2, userClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[1])); + userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(1, userClaims.Count); + IdentityResultAssert.IsSuccess(await manager.RemoveClaimAsync(user, claims[2])); + userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(0, userClaims.Count); + var userClaims2 = await manager.GetClaimsAsync(user2); + Assert.Equal(3, userClaims2.Count); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanReplaceUserClaim() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, new Claim("c", "a"))); + var userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(1, userClaims.Count); + Claim claim = new Claim("c", "b"); + Claim oldClaim = userClaims.FirstOrDefault(); + IdentityResultAssert.IsSuccess(await manager.ReplaceClaimAsync(user, oldClaim, claim)); + var newUserClaims = await manager.GetClaimsAsync(user); + Assert.Equal(1, newUserClaims.Count); + Claim newClaim = newUserClaims.FirstOrDefault(); + Assert.Equal(claim.Type, newClaim.Type); + Assert.Equal(claim.Value, newClaim.Value); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task ReplaceUserClaimOnlyAffectsUser() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + var user2 = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user2)); + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, new Claim("c", "a"))); + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user2, new Claim("c", "a"))); + var userClaims = await manager.GetClaimsAsync(user); + Assert.Equal(1, userClaims.Count); + var userClaims2 = await manager.GetClaimsAsync(user); + Assert.Equal(1, userClaims2.Count); + Claim claim = new Claim("c", "b"); + Claim oldClaim = userClaims.FirstOrDefault(); + IdentityResultAssert.IsSuccess(await manager.ReplaceClaimAsync(user, oldClaim, claim)); + var newUserClaims = await manager.GetClaimsAsync(user); + Assert.Equal(1, newUserClaims.Count); + Claim newClaim = newUserClaims.FirstOrDefault(); + Assert.Equal(claim.Type, newClaim.Type); + Assert.Equal(claim.Value, newClaim.Value); + userClaims2 = await manager.GetClaimsAsync(user2); + Assert.Equal(1, userClaims2.Count); + Claim oldClaim2 = userClaims2.FirstOrDefault(); + Assert.Equal("c", oldClaim2.Type); + Assert.Equal("a", oldClaim2.Value); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task ChangePasswordFallsIfPasswordWrong() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, "password")); + var result = await manager.ChangePasswordAsync(user, "bogus", "newpassword"); + IdentityResultAssert.IsFailure(result, "Incorrect password."); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"Change password failed for user {await manager.GetUserIdAsync(user)}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task AddDupeUserNameFails() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var username = "AddDupeUserNameFails" + Guid.NewGuid(); + var user = CreateTestUser(username, useNamePrefixAsUserName: true); + var user2 = CreateTestUser(username, useNamePrefixAsUserName: true); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsFailure(await manager.CreateAsync(user2), _errorDescriber.DuplicateUserName(username)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task AddDupeEmailAllowedByDefault() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(email: "yup@yup.com"); + var user2 = CreateTestUser(email: "yup@yup.com"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user2)); + IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user2, await manager.GetEmailAsync(user))); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task AddDupeEmailFailsWhenUniqueEmailRequired() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + manager.Options.User.RequireUniqueEmail = true; + var userGuid = Guid.NewGuid().ToString("n") + "FooUser@yup.com"; + var user = CreateTestUser(email: userGuid); + var user2 = CreateTestUser(email: userGuid); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsFailure(await manager.CreateAsync(user2), _errorDescriber.DuplicateEmail(userGuid)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task UpdateSecurityStampActuallyChanges() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + Assert.Null(await manager.GetSecurityStampAsync(user)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var stamp = await manager.GetSecurityStampAsync(user); + Assert.NotNull(stamp); + IdentityResultAssert.IsSuccess(await manager.UpdateSecurityStampAsync(user)); + Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task AddDupeLoginFails() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + var guid = Guid.NewGuid().ToString(); + var login = new UserLoginInfo("Provider", guid, "display"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.AddLoginAsync(user, login)); + var result = await manager.AddLoginAsync(user, login); + IdentityResultAssert.IsFailure(result, _errorDescriber.LoginAlreadyAssociated()); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"AddLogin for user {await manager.GetUserIdAsync(user)} failed because it was already assocated with another user."); + } + + // Email tests + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanFindByEmail() + { + if (ShouldSkipDbTests()) + { + return; + } + var email = "foouser@test.com" + Guid.NewGuid(); + var manager = CreateManager(); + var user = CreateTestUser(email: email); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var fetch = await manager.FindByEmailAsync(email); + Assert.Equal(user.Id, fetch.Id); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanFindUsersViaUserQuerable() + { + if (ShouldSkipDbTests()) + { + return; + } + var mgr = CreateManager(); + if (mgr.SupportsQueryableUsers) + { + var guidName = Guid.NewGuid().ToString("n") + "CanFindUsersViaUserQuerable"; + var users = GenerateUsers(guidName, 4); + foreach (var u in users) + { + IdentityResultAssert.IsSuccess(await mgr.CreateAsync(u)); + } + Assert.Equal(users.Count, mgr.Users.Count(UserNameStartsWithPredicate(guidName))); + Assert.Null(mgr.Users.FirstOrDefault(UserNameEqualsPredicate("bogus"))); + } + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task ConfirmEmailFalseByDefaultTest() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + Assert.False(await manager.IsEmailConfirmedAsync(user)); + } + + private class StaticTokenProvider : IUserTwoFactorTokenProvider + { + public async Task GenerateAsync(string purpose, UserManager manager, TUser user) + { + return MakeToken(purpose, await manager.GetUserIdAsync(user)); + } + + public async Task ValidateAsync(string purpose, string token, UserManager manager, TUser user) + { + return token == MakeToken(purpose, await manager.GetUserIdAsync(user)); + } + + public Task CanGenerateTwoFactorTokenAsync(UserManager manager, TUser user) + { + return Task.FromResult(true); + } + + private static string MakeToken(string purpose, string userId) + { + return string.Join(":", userId, purpose, "ImmaToken"); + } + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanResetPasswordWithStaticTokenProvider() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + manager.RegisterTokenProvider("Static", new StaticTokenProvider()); + manager.Options.Tokens.PasswordResetTokenProvider = "Static"; + var user = CreateTestUser(); + const string password = "password"; + const string newPassword = "newpassword"; + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, password)); + var stamp = await manager.GetSecurityStampAsync(user); + Assert.NotNull(stamp); + var token = await manager.GeneratePasswordResetTokenAsync(user); + Assert.NotNull(token); + var userId = await manager.GetUserIdAsync(user); + IdentityResultAssert.IsSuccess(await manager.ResetPasswordAsync(user, token, newPassword)); + Assert.False(await manager.CheckPasswordAsync(user, password)); + Assert.True(await manager.CheckPasswordAsync(user, newPassword)); + Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task PasswordValidatorCanBlockResetPasswordWithStaticTokenProvider() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + manager.RegisterTokenProvider("Static", new StaticTokenProvider()); + manager.Options.Tokens.PasswordResetTokenProvider = "Static"; + var user = CreateTestUser(); + const string password = "password"; + const string newPassword = "newpassword"; + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, password)); + var stamp = await manager.GetSecurityStampAsync(user); + Assert.NotNull(stamp); + var token = await manager.GeneratePasswordResetTokenAsync(user); + Assert.NotNull(token); + manager.PasswordValidators.Add(new AlwaysBadValidator()); + IdentityResultAssert.IsFailure(await manager.ResetPasswordAsync(user, token, newPassword), + AlwaysBadValidator.ErrorMessage); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"User {await manager.GetUserIdAsync(user)} password validation failed: {AlwaysBadValidator.ErrorMessage.Code}."); + Assert.True(await manager.CheckPasswordAsync(user, password)); + Assert.Equal(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task ResetPasswordWithStaticTokenProviderFailsWithWrongToken() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + manager.RegisterTokenProvider("Static", new StaticTokenProvider()); + manager.Options.Tokens.PasswordResetTokenProvider = "Static"; + var user = CreateTestUser(); + const string password = "password"; + const string newPassword = "newpassword"; + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, password)); + var stamp = await manager.GetSecurityStampAsync(user); + Assert.NotNull(stamp); + IdentityResultAssert.IsFailure(await manager.ResetPasswordAsync(user, "bogus", newPassword), "Invalid token."); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"VerifyUserTokenAsync() failed with purpose: ResetPassword for user { await manager.GetUserIdAsync(user)}."); + Assert.True(await manager.CheckPasswordAsync(user, password)); + Assert.Equal(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanGenerateAndVerifyUserTokenWithStaticTokenProvider() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + manager.RegisterTokenProvider("Static", new StaticTokenProvider()); + var user = CreateTestUser(); + var user2 = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user2)); + var userId = await manager.GetUserIdAsync(user); + var token = await manager.GenerateUserTokenAsync(user, "Static", "test"); + + Assert.True(await manager.VerifyUserTokenAsync(user, "Static", "test", token)); + + Assert.False(await manager.VerifyUserTokenAsync(user, "Static", "test2", token)); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"VerifyUserTokenAsync() failed with purpose: test2 for user { await manager.GetUserIdAsync(user)}."); + + Assert.False(await manager.VerifyUserTokenAsync(user, "Static", "test", token + "a")); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"VerifyUserTokenAsync() failed with purpose: test for user { await manager.GetUserIdAsync(user)}."); + + Assert.False(await manager.VerifyUserTokenAsync(user2, "Static", "test", token)); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"VerifyUserTokenAsync() failed with purpose: test for user { await manager.GetUserIdAsync(user2)}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanConfirmEmailWithStaticToken() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + manager.RegisterTokenProvider("Static", new StaticTokenProvider()); + manager.Options.Tokens.EmailConfirmationTokenProvider = "Static"; + var user = CreateTestUser(); + Assert.False(await manager.IsEmailConfirmedAsync(user)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var token = await manager.GenerateEmailConfirmationTokenAsync(user); + Assert.NotNull(token); + var userId = await manager.GetUserIdAsync(user); + IdentityResultAssert.IsSuccess(await manager.ConfirmEmailAsync(user, token)); + Assert.True(await manager.IsEmailConfirmedAsync(user)); + IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, null)); + Assert.False(await manager.IsEmailConfirmedAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task ConfirmEmailWithStaticTokenFailsWithWrongToken() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + manager.RegisterTokenProvider("Static", new StaticTokenProvider()); + manager.Options.Tokens.EmailConfirmationTokenProvider = "Static"; + var user = CreateTestUser(); + Assert.False(await manager.IsEmailConfirmedAsync(user)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + IdentityResultAssert.IsFailure(await manager.ConfirmEmailAsync(user, "bogus"), "Invalid token."); + Assert.False(await manager.IsEmailConfirmedAsync(user)); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"VerifyUserTokenAsync() failed with purpose: EmailConfirmation for user { await manager.GetUserIdAsync(user)}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task ConfirmTokenFailsAfterPasswordChange() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(namePrefix: "Test"); + Assert.False(await manager.IsEmailConfirmedAsync(user)); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, "password")); + var token = await manager.GenerateEmailConfirmationTokenAsync(user); + Assert.NotNull(token); + IdentityResultAssert.IsSuccess(await manager.ChangePasswordAsync(user, "password", "newpassword")); + IdentityResultAssert.IsFailure(await manager.ConfirmEmailAsync(user, token), "Invalid token."); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"VerifyUserTokenAsync() failed with purpose: EmailConfirmation for user { await manager.GetUserIdAsync(user)}."); + Assert.False(await manager.IsEmailConfirmedAsync(user)); + } + + // Lockout tests + + /// + /// Test. + /// + /// Task + [Fact] + public async Task SingleFailureLockout() + { + if (ShouldSkipDbTests()) + { + return; + } + var mgr = CreateManager(); + mgr.Options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromHours(1); + mgr.Options.Lockout.MaxFailedAccessAttempts = 0; + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await mgr.CreateAsync(user)); + Assert.True(await mgr.GetLockoutEnabledAsync(user)); + Assert.False(await mgr.IsLockedOutAsync(user)); + IdentityResultAssert.IsSuccess(await mgr.AccessFailedAsync(user)); + Assert.True(await mgr.IsLockedOutAsync(user)); + Assert.True(await mgr.GetLockoutEndDateAsync(user) > DateTimeOffset.UtcNow.AddMinutes(55)); + IdentityResultAssert.VerifyLogMessage(mgr.Logger, $"User {await mgr.GetUserIdAsync(user)} is locked out."); + + Assert.Equal(0, await mgr.GetAccessFailedCountAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task TwoFailureLockout() + { + if (ShouldSkipDbTests()) + { + return; + } + var mgr = CreateManager(); + mgr.Options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromHours(1); + mgr.Options.Lockout.MaxFailedAccessAttempts = 2; + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await mgr.CreateAsync(user)); + Assert.True(await mgr.GetLockoutEnabledAsync(user)); + Assert.False(await mgr.IsLockedOutAsync(user)); + IdentityResultAssert.IsSuccess(await mgr.AccessFailedAsync(user)); + Assert.False(await mgr.IsLockedOutAsync(user)); + Assert.False(await mgr.GetLockoutEndDateAsync(user) > DateTimeOffset.UtcNow.AddMinutes(55)); + Assert.Equal(1, await mgr.GetAccessFailedCountAsync(user)); + IdentityResultAssert.IsSuccess(await mgr.AccessFailedAsync(user)); + Assert.True(await mgr.IsLockedOutAsync(user)); + Assert.True(await mgr.GetLockoutEndDateAsync(user) > DateTimeOffset.UtcNow.AddMinutes(55)); + IdentityResultAssert.VerifyLogMessage(mgr.Logger, $"User {await mgr.GetUserIdAsync(user)} is locked out."); + Assert.Equal(0, await mgr.GetAccessFailedCountAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task ResetAccessCountPreventsLockout() + { + if (ShouldSkipDbTests()) + { + return; + } + var mgr = CreateManager(); + mgr.Options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromHours(1); + mgr.Options.Lockout.MaxFailedAccessAttempts = 2; + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await mgr.CreateAsync(user)); + Assert.True(await mgr.GetLockoutEnabledAsync(user)); + Assert.False(await mgr.IsLockedOutAsync(user)); + IdentityResultAssert.IsSuccess(await mgr.AccessFailedAsync(user)); + Assert.False(await mgr.IsLockedOutAsync(user)); + Assert.False(await mgr.GetLockoutEndDateAsync(user) > DateTimeOffset.UtcNow.AddMinutes(55)); + Assert.Equal(1, await mgr.GetAccessFailedCountAsync(user)); + IdentityResultAssert.IsSuccess(await mgr.ResetAccessFailedCountAsync(user)); + Assert.Equal(0, await mgr.GetAccessFailedCountAsync(user)); + Assert.False(await mgr.IsLockedOutAsync(user)); + Assert.False(await mgr.GetLockoutEndDateAsync(user) > DateTimeOffset.UtcNow.AddMinutes(55)); + IdentityResultAssert.IsSuccess(await mgr.AccessFailedAsync(user)); + Assert.False(await mgr.IsLockedOutAsync(user)); + Assert.False(await mgr.GetLockoutEndDateAsync(user) > DateTimeOffset.UtcNow.AddMinutes(55)); + Assert.Equal(1, await mgr.GetAccessFailedCountAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanEnableLockoutManuallyAndLockout() + { + if (ShouldSkipDbTests()) + { + return; + } + var mgr = CreateManager(); + mgr.Options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromHours(1); + mgr.Options.Lockout.AllowedForNewUsers = false; + mgr.Options.Lockout.MaxFailedAccessAttempts = 2; + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await mgr.CreateAsync(user)); + Assert.False(await mgr.GetLockoutEnabledAsync(user)); + IdentityResultAssert.IsSuccess(await mgr.SetLockoutEnabledAsync(user, true)); + Assert.True(await mgr.GetLockoutEnabledAsync(user)); + Assert.False(await mgr.IsLockedOutAsync(user)); + IdentityResultAssert.IsSuccess(await mgr.AccessFailedAsync(user)); + Assert.False(await mgr.IsLockedOutAsync(user)); + Assert.False(await mgr.GetLockoutEndDateAsync(user) > DateTimeOffset.UtcNow.AddMinutes(55)); + Assert.Equal(1, await mgr.GetAccessFailedCountAsync(user)); + IdentityResultAssert.IsSuccess(await mgr.AccessFailedAsync(user)); + Assert.True(await mgr.IsLockedOutAsync(user)); + Assert.True(await mgr.GetLockoutEndDateAsync(user) > DateTimeOffset.UtcNow.AddMinutes(55)); + IdentityResultAssert.VerifyLogMessage(mgr.Logger, $"User {await mgr.GetUserIdAsync(user)} is locked out."); + Assert.Equal(0, await mgr.GetAccessFailedCountAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task UserNotLockedOutWithNullDateTimeAndIsSetToNullDate() + { + if (ShouldSkipDbTests()) + { + return; + } + var mgr = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await mgr.CreateAsync(user)); + Assert.True(await mgr.GetLockoutEnabledAsync(user)); + IdentityResultAssert.IsSuccess(await mgr.SetLockoutEndDateAsync(user, new DateTimeOffset())); + Assert.False(await mgr.IsLockedOutAsync(user)); + Assert.Equal(new DateTimeOffset(), await mgr.GetLockoutEndDateAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task LockoutFailsIfNotEnabled() + { + if (ShouldSkipDbTests()) + { + return; + } + var mgr = CreateManager(); + mgr.Options.Lockout.AllowedForNewUsers = false; + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await mgr.CreateAsync(user)); + Assert.False(await mgr.GetLockoutEnabledAsync(user)); + IdentityResultAssert.IsFailure(await mgr.SetLockoutEndDateAsync(user, new DateTimeOffset()), + "Lockout is not enabled for this user."); + IdentityResultAssert.VerifyLogMessage(mgr.Logger, $"Lockout for user {await mgr.GetUserIdAsync(user)} failed because lockout is not enabled for this user."); + Assert.False(await mgr.IsLockedOutAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task LockoutEndToUtcNowMinus1SecInUserShouldNotBeLockedOut() + { + if (ShouldSkipDbTests()) + { + return; + } + var mgr = CreateManager(); + var user = CreateTestUser(lockoutEnd: DateTimeOffset.UtcNow.AddSeconds(-1)); + IdentityResultAssert.IsSuccess(await mgr.CreateAsync(user)); + Assert.True(await mgr.GetLockoutEnabledAsync(user)); + Assert.False(await mgr.IsLockedOutAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task LockoutEndToUtcNowSubOneSecondWithManagerShouldNotBeLockedOut() + { + if (ShouldSkipDbTests()) + { + return; + } + var mgr = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await mgr.CreateAsync(user)); + Assert.True(await mgr.GetLockoutEnabledAsync(user)); + IdentityResultAssert.IsSuccess(await mgr.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddSeconds(-1))); + Assert.False(await mgr.IsLockedOutAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task LockoutEndToUtcNowPlus5ShouldBeLockedOut() + { + if (ShouldSkipDbTests()) + { + return; + } + var mgr = CreateManager(); + var lockoutEnd = DateTimeOffset.UtcNow.AddMinutes(5); + var user = CreateTestUser(lockoutEnd: lockoutEnd); + IdentityResultAssert.IsSuccess(await mgr.CreateAsync(user)); + Assert.True(await mgr.GetLockoutEnabledAsync(user)); + Assert.True(await mgr.IsLockedOutAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task UserLockedOutWithDateTimeLocalKindNowPlus30() + { + if (ShouldSkipDbTests()) + { + return; + } + var mgr = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await mgr.CreateAsync(user)); + Assert.True(await mgr.GetLockoutEnabledAsync(user)); + var lockoutEnd = new DateTimeOffset(DateTime.Now.AddMinutes(30).ToLocalTime()); + IdentityResultAssert.IsSuccess(await mgr.SetLockoutEndDateAsync(user, lockoutEnd)); + Assert.True(await mgr.IsLockedOutAsync(user)); + var end = await mgr.GetLockoutEndDateAsync(user); + Assert.Equal(lockoutEnd, end); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task SetPhoneNumberTest() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(phoneNumber: "123-456-7890"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var stamp = await manager.GetSecurityStampAsync(user); + Assert.Equal("123-456-7890", await manager.GetPhoneNumberAsync(user)); + IdentityResultAssert.IsSuccess(await manager.SetPhoneNumberAsync(user, "111-111-1111")); + Assert.Equal("111-111-1111", await manager.GetPhoneNumberAsync(user)); + Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanChangePhoneNumber() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(phoneNumber: "123-456-7890"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + Assert.False(await manager.IsPhoneNumberConfirmedAsync(user)); + var stamp = await manager.GetSecurityStampAsync(user); + var token1 = await manager.GenerateChangePhoneNumberTokenAsync(user, "111-111-1111"); + IdentityResultAssert.IsSuccess(await manager.ChangePhoneNumberAsync(user, "111-111-1111", token1)); + Assert.True(await manager.IsPhoneNumberConfirmedAsync(user)); + Assert.Equal("111-111-1111", await manager.GetPhoneNumberAsync(user)); + Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task ChangePhoneNumberFailsWithWrongToken() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(phoneNumber: "123-456-7890"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + Assert.False(await manager.IsPhoneNumberConfirmedAsync(user)); + var stamp = await manager.GetSecurityStampAsync(user); + IdentityResultAssert.IsFailure(await manager.ChangePhoneNumberAsync(user, "111-111-1111", "bogus"), + "Invalid token."); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"VerifyUserTokenAsync() failed with purpose: ChangePhoneNumber:111-111-1111 for user { await manager.GetUserIdAsync(user)}."); + Assert.False(await manager.IsPhoneNumberConfirmedAsync(user)); + Assert.Equal("123-456-7890", await manager.GetPhoneNumberAsync(user)); + Assert.Equal(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task ChangePhoneNumberFailsWithWrongPhoneNumber() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(phoneNumber: "123-456-7890"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + Assert.False(await manager.IsPhoneNumberConfirmedAsync(user)); + var stamp = await manager.GetSecurityStampAsync(user); + var token1 = await manager.GenerateChangePhoneNumberTokenAsync(user, "111-111-1111"); + IdentityResultAssert.IsFailure(await manager.ChangePhoneNumberAsync(user, "bogus", token1), + "Invalid token."); + Assert.False(await manager.IsPhoneNumberConfirmedAsync(user)); + Assert.Equal("123-456-7890", await manager.GetPhoneNumberAsync(user)); + Assert.Equal(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanVerifyPhoneNumber() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + const string num1 = "111-123-4567"; + const string num2 = "111-111-1111"; + var userId = await manager.GetUserIdAsync(user); + var token1 = await manager.GenerateChangePhoneNumberTokenAsync(user, num1); + + var token2 = await manager.GenerateChangePhoneNumberTokenAsync(user, num2); + Assert.NotEqual(token1, token2); + Assert.True(await manager.VerifyChangePhoneNumberTokenAsync(user, token1, num1)); + Assert.True(await manager.VerifyChangePhoneNumberTokenAsync(user, token2, num2)); + Assert.False(await manager.VerifyChangePhoneNumberTokenAsync(user, token2, num1)); + Assert.False(await manager.VerifyChangePhoneNumberTokenAsync(user, token1, num2)); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"VerifyUserTokenAsync() failed with purpose: ChangePhoneNumber:111-123-4567 for user {await manager.GetUserIdAsync(user)}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanChangeEmail() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser("foouser"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var email = await manager.GetUserNameAsync(user) + "@diddly.bop"; + IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, email)); + Assert.False(await manager.IsEmailConfirmedAsync(user)); + var stamp = await manager.GetSecurityStampAsync(user); + var newEmail = await manager.GetUserNameAsync(user) + "@en.vec"; + var token1 = await manager.GenerateChangeEmailTokenAsync(user, newEmail); + IdentityResultAssert.IsSuccess(await manager.ChangeEmailAsync(user, newEmail, token1)); + Assert.True(await manager.IsEmailConfirmedAsync(user)); + Assert.Equal(await manager.GetEmailAsync(user), newEmail); + Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanChangeEmailWithDifferentTokenProvider() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(context: null, services: null, + configureServices: s => s.Configure( + o => o.Tokens.ProviderMap["NewProvider2"] = new TokenProviderDescriptor(typeof(EmailTokenProvider)))); + manager.Options.Tokens.ChangeEmailTokenProvider = "NewProvider2"; + var user = CreateTestUser("foouser"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var email = await manager.GetUserNameAsync(user) + "@diddly.bop"; + IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, email)); + Assert.False(await manager.IsEmailConfirmedAsync(user)); + var stamp = await manager.GetSecurityStampAsync(user); + var newEmail = await manager.GetUserNameAsync(user) + "@en.vec"; + var token1 = await manager.GenerateChangeEmailTokenAsync(user, newEmail); + IdentityResultAssert.IsSuccess(await manager.ChangeEmailAsync(user, newEmail, token1)); + Assert.True(await manager.IsEmailConfirmedAsync(user)); + Assert.Equal(await manager.GetEmailAsync(user), newEmail); + Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task ChangeEmailTokensFailsAfterEmailChanged() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser("foouser"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var email = await manager.GetUserNameAsync(user) + "@diddly.bop"; + IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, email)); + Assert.False(await manager.IsEmailConfirmedAsync(user)); + var stamp = await manager.GetSecurityStampAsync(user); + var newEmail = await manager.GetUserNameAsync(user) + "@en.vec"; + var token1 = await manager.GenerateChangeEmailTokenAsync(user, newEmail); + IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, "another@email.com")); + Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user)); + IdentityResultAssert.IsFailure(await manager.ChangeEmailAsync(user, newEmail, token1)); + Assert.False(await manager.IsEmailConfirmedAsync(user)); + Assert.Equal("another@email.com", await manager.GetEmailAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task ChangeEmailFailsWithWrongToken() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser("foouser"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var email = await manager.GetUserNameAsync(user) + "@diddly.bop"; + IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, email)); + string oldEmail = email; + Assert.False(await manager.IsEmailConfirmedAsync(user)); + var stamp = await manager.GetSecurityStampAsync(user); + IdentityResultAssert.IsFailure(await manager.ChangeEmailAsync(user, "whatevah@foo.boop", "bogus"), + "Invalid token."); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"VerifyUserTokenAsync() failed with purpose: ChangeEmail:whatevah@foo.boop for user { await manager.GetUserIdAsync(user)}."); + Assert.False(await manager.IsEmailConfirmedAsync(user)); + Assert.Equal(await manager.GetEmailAsync(user), oldEmail); + Assert.Equal(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task ChangeEmailFailsWithEmail() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser("foouser"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var email = await manager.GetUserNameAsync(user) + "@diddly.bop"; + IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, email)); + string oldEmail = email; + Assert.False(await manager.IsEmailConfirmedAsync(user)); + var stamp = await manager.GetSecurityStampAsync(user); + var token1 = await manager.GenerateChangeEmailTokenAsync(user, "forgot@alrea.dy"); + IdentityResultAssert.IsFailure(await manager.ChangeEmailAsync(user, "oops@foo.boop", token1), + "Invalid token."); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"VerifyUserTokenAsync() failed with purpose: ChangeEmail:oops@foo.boop for user { await manager.GetUserIdAsync(user)}."); + Assert.False(await manager.IsEmailConfirmedAsync(user)); + Assert.Equal(await manager.GetEmailAsync(user), oldEmail); + Assert.Equal(stamp, await manager.GetSecurityStampAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task EmailFactorFailsAfterSecurityStampChangeTest() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + string factorId = "Email"; //default + var user = CreateTestUser("foouser"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var email = await manager.GetUserNameAsync(user) + "@diddly.bop"; + IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, email)); + var token = await manager.GenerateEmailConfirmationTokenAsync(user); + await manager.ConfirmEmailAsync(user, token); + + var stamp = await manager.GetSecurityStampAsync(user); + Assert.NotNull(stamp); + token = await manager.GenerateTwoFactorTokenAsync(user, factorId); + Assert.NotNull(token); + IdentityResultAssert.IsSuccess(await manager.UpdateSecurityStampAsync(user)); + Assert.False(await manager.VerifyTwoFactorTokenAsync(user, factorId, token)); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"VerifyTwoFactorTokenAsync() failed for user {await manager.GetUserIdAsync(user)}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task EnableTwoFactorChangesSecurityStamp() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var stamp = await manager.GetSecurityStampAsync(user); + Assert.NotNull(stamp); + IdentityResultAssert.IsSuccess(await manager.SetTwoFactorEnabledAsync(user, true)); + Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user)); + Assert.True(await manager.GetTwoFactorEnabledAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task GenerateTwoFactorWithUnknownFactorProviderWillThrow() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + const string error = "No IUserTokenProvider named 'bogus' is registered."; + var ex = await + Assert.ThrowsAsync( + () => manager.GenerateTwoFactorTokenAsync(user, "bogus")); + Assert.Equal(error, ex.Message); + ex = await Assert.ThrowsAsync( + () => manager.VerifyTwoFactorTokenAsync(user, "bogus", "bogus")); + Assert.Equal(error, ex.Message); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task GetValidTwoFactorTestEmptyWithNoProviders() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var factors = await manager.GetValidTwoFactorProvidersAsync(user); + Assert.NotNull(factors); + Assert.True(!factors.Any()); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanGetSetUpdateAndRemoveUserToken() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + Assert.Null(await manager.GetAuthenticationTokenAsync(user, "provider", "name")); + IdentityResultAssert.IsSuccess(await manager.SetAuthenticationTokenAsync(user, "provider", "name", "value")); + var token = await manager.GetAuthenticationTokenAsync(user, "provider", "name"); + Assert.Equal("value", token); + + IdentityResultAssert.IsSuccess(await manager.SetAuthenticationTokenAsync(user, "provider", "name", "value2")); + Assert.Equal("value2", await manager.GetAuthenticationTokenAsync(user, "provider", "name")); + + IdentityResultAssert.IsSuccess(await manager.RemoveAuthenticationTokenAsync(user, "whatevs", "name")); + Assert.Equal("value2", await manager.GetAuthenticationTokenAsync(user, "provider", "name")); + + IdentityResultAssert.IsSuccess(await manager.RemoveAuthenticationTokenAsync(user, "provider", "name")); + Assert.Null(await manager.GetAuthenticationTokenAsync(user, "provider", "name")); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanRedeemRecoveryCodeOnlyOnce() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + + var numCodes = 15; + var newCodes = await manager.GenerateNewTwoFactorRecoveryCodesAsync(user, numCodes); + Assert.Equal(numCodes, newCodes.Count()); + + foreach (var code in newCodes) + { + IdentityResultAssert.IsSuccess(await manager.RedeemTwoFactorRecoveryCodeAsync(user, code)); + IdentityResultAssert.IsFailure(await manager.RedeemTwoFactorRecoveryCodeAsync(user, code)); + Assert.Equal(--numCodes, await manager.CountRecoveryCodesAsync(user)); + } + // One last time to be sure + foreach (var code in newCodes) + { + IdentityResultAssert.IsFailure(await manager.RedeemTwoFactorRecoveryCodeAsync(user, code)); + } + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task RecoveryCodesInvalidAfterReplace() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + + var numCodes = 15; + var newCodes = await manager.GenerateNewTwoFactorRecoveryCodesAsync(user, numCodes); + Assert.Equal(numCodes, newCodes.Count()); + var realCodes = await manager.GenerateNewTwoFactorRecoveryCodesAsync(user, numCodes); + Assert.Equal(numCodes, realCodes.Count()); + + foreach (var code in newCodes) + { + IdentityResultAssert.IsFailure(await manager.RedeemTwoFactorRecoveryCodeAsync(user, code)); + } + + foreach (var code in realCodes) + { + IdentityResultAssert.IsSuccess(await manager.RedeemTwoFactorRecoveryCodeAsync(user, code)); + } + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanGetValidTwoFactor() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var userId = await manager.GetUserIdAsync(user); + var factors = await manager.GetValidTwoFactorProvidersAsync(user); + Assert.NotNull(factors); + Assert.False(factors.Any()); + IdentityResultAssert.IsSuccess(await manager.SetPhoneNumberAsync(user, "111-111-1111")); + var token = await manager.GenerateChangePhoneNumberTokenAsync(user, "111-111-1111"); + IdentityResultAssert.IsSuccess(await manager.ChangePhoneNumberAsync(user, "111-111-1111", token)); + await manager.UpdateAsync(user); + factors = await manager.GetValidTwoFactorProvidersAsync(user); + Assert.NotNull(factors); + Assert.Single(factors); + Assert.Equal("Phone", factors[0]); + IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, "test@test.com")); + token = await manager.GenerateEmailConfirmationTokenAsync(user); + await manager.ConfirmEmailAsync(user, token); + factors = await manager.GetValidTwoFactorProvidersAsync(user); + Assert.NotNull(factors); + Assert.Equal(2, factors.Count()); + IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, null)); + factors = await manager.GetValidTwoFactorProvidersAsync(user); + Assert.NotNull(factors); + Assert.Single(factors); + Assert.Equal("Phone", factors[0]); + IdentityResultAssert.IsSuccess(await manager.ResetAuthenticatorKeyAsync(user)); + factors = await manager.GetValidTwoFactorProvidersAsync(user); + Assert.NotNull(factors); + Assert.Equal(2, factors.Count()); + Assert.Equal("Authenticator", factors[1]); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task PhoneFactorFailsAfterSecurityStampChangeTest() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var factorId = "Phone"; // default + var user = CreateTestUser(phoneNumber: "4251234567"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var stamp = await manager.GetSecurityStampAsync(user); + Assert.NotNull(stamp); + var token = await manager.GenerateTwoFactorTokenAsync(user, factorId); + Assert.NotNull(token); + IdentityResultAssert.IsSuccess(await manager.UpdateSecurityStampAsync(user)); + Assert.False(await manager.VerifyTwoFactorTokenAsync(user, factorId, token)); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"VerifyTwoFactorTokenAsync() failed for user {await manager.GetUserIdAsync(user)}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task VerifyTokenFromWrongTokenProviderFails() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(phoneNumber: "4251234567"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + var token = await manager.GenerateTwoFactorTokenAsync(user, "Phone"); + Assert.NotNull(token); + Assert.False(await manager.VerifyTwoFactorTokenAsync(user, "Email", token)); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"VerifyTwoFactorTokenAsync() failed for user {await manager.GetUserIdAsync(user)}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task VerifyWithWrongSmsTokenFails() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var user = CreateTestUser(phoneNumber: "4251234567"); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + Assert.False(await manager.VerifyTwoFactorTokenAsync(user, "Phone", "bogus")); + IdentityResultAssert.VerifyLogMessage(manager.Logger, $"VerifyTwoFactorTokenAsync() failed for user {await manager.GetUserIdAsync(user)}."); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task NullableDateTimeOperationTest() + { + if (ShouldSkipDbTests()) + { + return; + } + var userMgr = CreateManager(); + var user = CreateTestUser(lockoutEnabled: true); + IdentityResultAssert.IsSuccess(await userMgr.CreateAsync(user)); + + Assert.Null(await userMgr.GetLockoutEndDateAsync(user)); + + // set LockoutDateEndDate to null + await userMgr.SetLockoutEndDateAsync(user, null); + Assert.Null(await userMgr.GetLockoutEndDateAsync(user)); + + // set to a valid value + await userMgr.SetLockoutEndDateAsync(user, DateTimeOffset.Parse("01/01/2014")); + Assert.Equal(DateTimeOffset.Parse("01/01/2014"), await userMgr.GetLockoutEndDateAsync(user)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanGetUsersWithClaims() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var guid = Guid.NewGuid().ToString(); + + for (int i = 0; i < 6; i++) + { + var user = CreateTestUser(); + IdentityResultAssert.IsSuccess(await manager.CreateAsync(user)); + + if ((i % 2) == 0) + { + IdentityResultAssert.IsSuccess(await manager.AddClaimAsync(user, new Claim(guid, "bar"))); + } + } + + Assert.Equal(3, (await manager.GetUsersForClaimAsync(new Claim(guid, "bar"))).Count); + + Assert.Equal(0, (await manager.GetUsersForClaimAsync(new Claim("123", "456"))).Count); + } + + /// + /// Generate count users with a name prefix. + /// + /// + /// + /// + protected List GenerateUsers(string userNamePrefix, int count) + { + var users = new List(count); + for (var i = 0; i < count; i++) + { + users.Add(CreateTestUser(userNamePrefix + i)); + } + return users; + } + } +} \ No newline at end of file