Files
Umbraco-CMS/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs
Andreas Zerbst 7f1cdf8ef5 Auhorization: Cherrypicked integration tests from V15 (#20492)
* V15 QA Added the authorization integration tests (#18419)

* Added authorization integration tests

* Removed unnecessary tests and update tests for preview controller

* Updated to use the newest changes from v15/dev and added an override for the AuthenticateClientAsync to use the userGroupKey

* Updated CompatibilitySuppressions to include changes from integration tests

* Updated pipelines

* Skips managementApi tests

* Only run necessary tests

* Added new schema per fixture to reduce test setup time

* Fixed failing tests

* Updated test setup

* Updated test

* Added suppression

* Fixed failing tests

* Updated addOnTeardown methods to protected

* Added method for clearing the host

* Added teardown

* Updated model usage

* Added a lot of cleanup for memory leak issues when running tests

* Added CompatibilitySuppressions.xml

* Updated tests

* Cleaned up

* Adjusted base classes

* Updated pipeline

* Updated CompatibilitySuppressions.xml

* Updated test logging

* Fixed reponse

* Updated condition to skip tests

* Updated tests, not done

* Reworked test to expect correct responses with correct setup

* Updated tests

* More updates to tests

* Updated tests

* Cleaned up tests

* Updated setup

* Cleaned up tests to match setup

* Cleaned up setup

* Removed suppression

* Fixed tests

* Move order of checks

* Fix naming

* Formatting

* Dispose of host

* Keep track of if we're disposed

* Compat suppression

* Dont dispose

* Fix failing tests

* removed unused virtual

* Updated CompatibilitySuppressions.xml

---------

Co-authored-by: Andreas Zerbst <andr317c@live.dk>
Co-authored-by: Zeegaan <skrivdetud@gmail.com>
Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
# Conflicts:
#	tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml
#	tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs
#	tests/Umbraco.Tests.Integration/ManagementApi/Policies/AllCultureControllerTests.cs
#	tests/Umbraco.Tests.Integration/ManagementApi/Policies/CreateDocumentTests.cs
#	tests/Umbraco.Tests.Integration/ManagementApi/Policies/UpdateDocumentTests.cs
#	tests/Umbraco.Tests.Integration/ManagementApi/Preview/EndPreviewTests.cs
#	tests/Umbraco.Tests.Integration/ManagementApi/Preview/EnterPreviewTests.cs
#	tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs

* Updated test

* Updates

* Removed unnessecary test

---------

Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com>
Co-authored-by: Zeegaan <skrivdetud@gmail.com>
Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
2025-10-14 10:04:10 +00:00

210 lines
8.9 KiB
C#

using System.Linq.Expressions;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Net.Mime;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Serialization;
using System.Web;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using OpenIddict.Abstractions;
using Umbraco.Cms.Api.Management.Controllers;
using Umbraco.Cms.Api.Management.Controllers.Security;
using Umbraco.Cms.Api.Management.Security;
using Umbraco.Cms.Api.Management.ViewModels.Security;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Security;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.TestServerTest;
namespace Umbraco.Cms.Tests.Integration.ManagementApi;
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture, Logger = UmbracoTestOptions.Logger.Console, Boot = true)]
public abstract class ManagementApiTest<T> : UmbracoTestServerTestBase
where T : ManagementApiControllerBase
{
private static readonly Dictionary<string, TokenModel> _tokenCache = new();
private static readonly SHA256 _sha256 = SHA256.Create();
protected abstract Expression<Func<T, object>> MethodSelector { get; set; }
protected string Url => GetManagementApiUrl(MethodSelector);
[SetUp]
public override void Setup()
{
InMemoryConfiguration["Umbraco:CMS:ModelsBuilder:ModelsMode"] = "Nothing";
base.Setup();
Client.DefaultRequestHeaders.Accept.Clear();
Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
}
[SetUp]
public override void SetUp_Logging() =>
TestContext.Out.Write($"Start test {TestCount++}: {TestContext.CurrentContext.Test.FullName}");
[OneTimeTearDown]
public void ClearCache() => _tokenCache.Clear();
protected override void CustomTestAuthSetup(IServiceCollection services)
{
// We do not wanna fake anything, and thereby have protection
}
protected async Task AuthenticateClientAsync(HttpClient client, string username, string password, bool isAdmin) =>
await AuthenticateClientAsync(client,
async userService =>
{
IUser user;
if (isAdmin)
{
user = await userService.GetRequiredUserAsync(Constants.Security.SuperUserKey);
user.Username = user.Email = username;
userService.Save(user);
}
else
{
user = (await userService.CreateAsync(
Constants.Security.SuperUserKey,
new UserCreateModel
{
Email = username,
Name = username,
UserName = username,
UserGroupKeys = new HashSet<Guid>(new[] { Constants.Security.EditorGroupKey })
},
true)).Result.CreatedUser;
}
return (user, password);
}, $"{username}:{isAdmin}");
protected async Task AuthenticateClientAsync(HttpClient client, string username, string password, Guid userGroupKey) =>
await AuthenticateClientAsync(client,
async userService =>
{
IUser user;
if (userGroupKey == Constants.Security.AdminGroupKey)
{
user = await userService.GetRequiredUserAsync(Constants.Security.SuperUserKey);
user.Username = user.Email = username;
userService.Save(user);
}
else
{
user = (await userService.CreateAsync(
Constants.Security.SuperUserKey,
new UserCreateModel
{
Email = username,
Name = username,
UserName = username,
UserGroupKeys = new HashSet<Guid>([userGroupKey]),
},
true)).Result.CreatedUser;
}
return (user, password);
}, $"{username}:{userGroupKey}");
protected async Task AuthenticateClientAsync(HttpClient client, Func<IUserService, Task<(IUser User, string Password)>> createUser, string cacheKey = null)
{
// Check cache first
if (!string.IsNullOrEmpty(cacheKey) && _tokenCache.TryGetValue(cacheKey, out var cachedToken))
{
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", cachedToken.AccessToken);
return;
}
OpenIddictApplicationDescriptor backofficeOpenIddictApplicationDescriptor;
var scopeProvider = GetRequiredService<ICoreScopeProvider>();
string username;
string password;
using (var scope = scopeProvider.CreateCoreScope())
{
var userService = GetRequiredService<IUserService>();
using var serviceScope = GetRequiredService<IServiceScopeFactory>().CreateScope();
var userManager = serviceScope.ServiceProvider.GetRequiredService<ICoreBackOfficeUserManager>();
var userCreationResult = await createUser(userService);
username = userCreationResult.User.Username;
password = userCreationResult.Password;
var userKey = userCreationResult.User.Key;
var token = await userManager.GeneratePasswordResetTokenAsync(userCreationResult.User);
var changePasswordAttempt = await userService.ChangePasswordAsync(userKey,
new ChangeUserPasswordModel
{
NewPassword = password, ResetPasswordToken = token.Result.ToUrlBase64(), UserKey = userKey,
});
Assert.IsTrue(changePasswordAttempt.Success);
var backOfficeApplicationManager =
serviceScope.ServiceProvider.GetRequiredService<IBackOfficeApplicationManager>() as
BackOfficeApplicationManager;
backofficeOpenIddictApplicationDescriptor =
backOfficeApplicationManager.BackofficeOpenIddictApplicationDescriptor(client.BaseAddress);
scope.Complete();
}
var loginModel = new LoginRequestModel { Username = username, Password = password };
// Login to ensure the cookie is set (used in next request)
var loginResponse = await client.PostAsync(
GetManagementApiUrl<BackOfficeController>(x => x.Login(CancellationToken.None, null)), JsonContent.Create(loginModel));
Assert.AreEqual(HttpStatusCode.OK, loginResponse.StatusCode, await loginResponse.Content.ReadAsStringAsync());
const string codeVerifier = "12345"; // Just a dummy value we use in tests
var codeChallenge = Convert.ToBase64String(_sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier)))
.TrimEnd("=");
var authorizationUrl = GetManagementApiUrl<BackOfficeController>(x => x.Authorize(CancellationToken.None)) + $"?client_id={backofficeOpenIddictApplicationDescriptor.ClientId}&response_type=code&redirect_uri={WebUtility.UrlEncode(backofficeOpenIddictApplicationDescriptor.RedirectUris.FirstOrDefault()?.AbsoluteUri)}&code_challenge_method=S256&code_challenge={codeChallenge}";
var authorizeResponse = await client.GetAsync(authorizationUrl);
Assert.AreEqual(HttpStatusCode.Found, authorizeResponse.StatusCode, await authorizeResponse.Content.ReadAsStringAsync());
var tokenResponse = await client.PostAsync("/umbraco/management/api/v1/security/back-office/token",
new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "authorization_code",
["code_verifier"] = codeVerifier,
["client_id"] = backofficeOpenIddictApplicationDescriptor.ClientId,
["code"] = HttpUtility.ParseQueryString(authorizeResponse.Headers.Location.Query).Get("code"),
["redirect_uri"] =
backofficeOpenIddictApplicationDescriptor.RedirectUris.FirstOrDefault().AbsoluteUri,
}));
var tokenModel = await tokenResponse.Content.ReadFromJsonAsync<TokenModel>();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenModel.AccessToken);
// Cache the token if cache key provided
if (!string.IsNullOrEmpty(cacheKey))
{
_tokenCache[cacheKey] = tokenModel;
}
}
private class TokenModel
{
[JsonPropertyName("access_token")] public string AccessToken { get; set; }
}
}