Merge remote-tracking branch 'origin/release/17.0'

This commit is contained in:
Jacob Overgaard
2025-11-17 16:37:58 +01:00
28 changed files with 349 additions and 229 deletions

3
.github/BUILD.md vendored
View File

@@ -79,13 +79,12 @@ Conversely, if you are working on front-end only, you want to build the back-end
"AuthorizeCallbackLogoutPathName": "/logout",
"AuthorizeCallbackErrorPathName": "/error",
"BackOfficeTokenCookie": {
"Enabled": true,
"SameSite": "None"
}
```
> [!NOTE]
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `BackOfficeTokenCookie` settings are correct. Namely, that `SameSite` should be set to `None` when running the front-end server separately.
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `Umbraco:Cms:Security:BackOfficeTokenCookie:SameSite` setting is set to `None`.
Then run Umbraco from the command line.

View File

@@ -6,91 +6,101 @@ Always reference these instructions first and fallback to search or bash command
Bootstrap, build, and test the repository:
- Install .NET SDK (version specified in global.json):
- `curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --version $(jq -r '.sdk.version' global.json)`
- `export PATH="/home/runner/.dotnet:$PATH"`
- Install Node.js (version specified in src/Umbraco.Web.UI.Client/.nvmrc):
- `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash`
- `export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"`
- `nvm install $(cat src/Umbraco.Web.UI.Client/.nvmrc) && nvm use $(cat src/Umbraco.Web.UI.Client/.nvmrc)`
- Fix shallow clone issue (required for GitVersioning):
- `git fetch --unshallow`
- Restore packages:
- `dotnet restore` -- takes 50 seconds. NEVER CANCEL. Set timeout to 90+ seconds.
- Build the solution:
- `dotnet build` -- takes 4.5 minutes. NEVER CANCEL. Set timeout to 10+ minutes.
- Install and build frontend:
- `cd src/Umbraco.Web.UI.Client`
- `npm ci --no-fund --no-audit --prefer-offline` -- takes 11 seconds.
- `npm run build:for:cms` -- takes 1.25 minutes. NEVER CANCEL. Set timeout to 5+ minutes.
- Install and build Login
- `cd src/Umbraco.Web.UI.Login`
- `npm ci --no-fund --no-audit --prefer-offline`
- `npm run build`
- Run the application:
- `cd src/Umbraco.Web.UI`
- `dotnet run --no-build` -- Application runs on https://localhost:44339 and http://localhost:11000
- Install .NET SDK (version specified in global.json):
- `curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --version $(jq -r '.sdk.version' global.json)`
- `export PATH="/home/runner/.dotnet:$PATH"`
- Install Node.js (version specified in src/Umbraco.Web.UI.Client/.nvmrc):
- `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash`
- `export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"`
- `nvm install $(cat src/Umbraco.Web.UI.Client/.nvmrc) && nvm use $(cat src/Umbraco.Web.UI.Client/.nvmrc)`
- Fix shallow clone issue (required for GitVersioning):
- `git fetch --unshallow`
- Restore packages:
- `dotnet restore` -- takes 50 seconds. NEVER CANCEL. Set timeout to 90+ seconds.
- Build the solution:
- `dotnet build` -- takes 4.5 minutes. NEVER CANCEL. Set timeout to 10+ minutes.
- Install and build frontend:
- `cd src/Umbraco.Web.UI.Client`
- `npm ci --no-fund --no-audit --prefer-offline` -- takes 11 seconds.
- `npm run build:for:cms` -- takes 1.25 minutes. NEVER CANCEL. Set timeout to 5+ minutes.
- Install and build Login
- `cd src/Umbraco.Web.UI.Login`
- `npm ci --no-fund --no-audit --prefer-offline`
- `npm run build`
- Run the application:
- `cd src/Umbraco.Web.UI`
- `dotnet run --no-build` -- Application runs on https://localhost:44339 and http://localhost:11000
Check out [BUILD.md](./BUILD.md) for more detailed instructions.
## Validation
- ALWAYS run through at least one complete end-to-end scenario after making changes.
- Build and unit tests must pass before committing changes.
- Frontend build produces output in src/Umbraco.Web.UI.Client/dist-cms/ which gets copied to src/Umbraco.Web.UI/wwwroot/umbraco/backoffice/
- Always run `dotnet build` and `npm run build:for:cms` before running the application to see your changes.
- For login-only changes, you can run `npm run build` from src/Umbraco.Web.UI.Login and then `dotnet run --no-build` from src/Umbraco.Web.UI.
- For frontend-only changes, you can run `npm run dev:server` from src/Umbraco.Web.UI.Client for hot reloading.
- Frontend changes should be linted using `npm run lint:fix` which uses Eslint.
- ALWAYS run through at least one complete end-to-end scenario after making changes.
- Build and unit tests must pass before committing changes.
- Frontend build produces output in src/Umbraco.Web.UI.Client/dist-cms/ which gets copied to src/Umbraco.Web.UI/wwwroot/umbraco/backoffice/
- Always run `dotnet build` and `npm run build:for:cms` before running the application to see your changes.
- For login-only changes, you can run `npm run build` from src/Umbraco.Web.UI.Login and then `dotnet run --no-build` from src/Umbraco.Web.UI.
- For frontend-only changes, you can run `npm run dev:server` from src/Umbraco.Web.UI.Client for hot reloading.
- Frontend changes should be linted using `npm run lint:fix` which uses Eslint.
## Testing
### Unit Tests (.NET)
- Location: tests/Umbraco.Tests.UnitTests/
- Run: `dotnet test tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj --configuration Release --verbosity minimal`
- Duration: ~1 minute with 3,343 tests
- NEVER CANCEL: Set timeout to 5+ minutes
- Location: tests/Umbraco.Tests.UnitTests/
- Run: `dotnet test tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj --configuration Release --verbosity minimal`
- Duration: ~1 minute with 3,343 tests
- NEVER CANCEL: Set timeout to 5+ minutes
### Integration Tests (.NET)
- Location: tests/Umbraco.Tests.Integration/
- Run: `dotnet test tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj --configuration Release --verbosity minimal`
- NEVER CANCEL: Set timeout to 10+ minutes
- Location: tests/Umbraco.Tests.Integration/
- Run: `dotnet test tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj --configuration Release --verbosity minimal`
- NEVER CANCEL: Set timeout to 10+ minutes
### Frontend Tests
- Location: src/Umbraco.Web.UI.Client/
- Run: `npm test` (requires `npx playwright install` first)
- Frontend tests use Web Test Runner with Playwright
- Location: src/Umbraco.Web.UI.Client/
- Run: `npm test` (requires `npx playwright install` first)
- Frontend tests use Web Test Runner with Playwright
### Acceptance Tests (E2E)
- Location: tests/Umbraco.Tests.AcceptanceTest/
- Requires running Umbraco application and configuration
- See tests/Umbraco.Tests.AcceptanceTest/README.md for detailed setup (requires `npx playwright install` first)
- Location: tests/Umbraco.Tests.AcceptanceTest/
- Requires running Umbraco application and configuration
- See tests/Umbraco.Tests.AcceptanceTest/README.md for detailed setup (requires `npx playwright install` first)
## Project Structure
The solution contains 30 C# projects organized as follows:
### Main Application Projects
- **Umbraco.Web.UI**: Main web application project (startup project)
- **Umbraco.Web.UI.Client**: TypeScript frontend (backoffice)
- **Umbraco.Web.UI.Login**: Separate login screen frontend
- **Umbraco.Core**: Core domain models and interfaces
- **Umbraco.Infrastructure**: Data access and infrastructure
- **Umbraco.Cms**: Main CMS package
- **Umbraco.Web.UI**: Main web application project (startup project)
- **Umbraco.Web.UI.Client**: TypeScript frontend (backoffice)
- **Umbraco.Web.UI.Login**: Separate login screen frontend
- **Umbraco.Core**: Core domain models and interfaces
- **Umbraco.Infrastructure**: Data access and infrastructure
- **Umbraco.Cms**: Main CMS package
### API Projects
- **Umbraco.Cms.Api.Management**: Management API
- **Umbraco.Cms.Api.Delivery**: Content Delivery API
- **Umbraco.Cms.Api.Common**: Shared API components
- **Umbraco.Cms.Api.Management**: Management API
- **Umbraco.Cms.Api.Delivery**: Content Delivery API
- **Umbraco.Cms.Api.Common**: Shared API components
### Persistence Projects
- **Umbraco.Cms.Persistence.SqlServer**: SQL Server support
- **Umbraco.Cms.Persistence.Sqlite**: SQLite support
- **Umbraco.Cms.Persistence.EFCore**: Entity Framework Core abstractions
- **Umbraco.Cms.Persistence.SqlServer**: SQL Server support
- **Umbraco.Cms.Persistence.Sqlite**: SQLite support
- **Umbraco.Cms.Persistence.EFCore**: Entity Framework Core abstractions
### Test Projects
- **Umbraco.Tests.UnitTests**: Unit tests
- **Umbraco.Tests.Integration**: Integration tests
- **Umbraco.Tests.AcceptanceTest**: End-to-end tests with Playwright
- **Umbraco.Tests.Common**: Shared test utilities
- **Umbraco.Tests.UnitTests**: Unit tests
- **Umbraco.Tests.Integration**: Integration tests
- **Umbraco.Tests.AcceptanceTest**: End-to-end tests with Playwright
- **Umbraco.Tests.Common**: Shared test utilities
## Common Tasks
@@ -98,6 +108,7 @@ The solution contains 30 C# projects organized as follows:
**Production Mode (Standard Development)**
Use this for backend development, testing full builds, or when you don't need hot reloading:
1. Build frontend assets: `cd src/Umbraco.Web.UI.Client && npm run build:for:cms`
2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build`
3. Access backoffice: `https://localhost:44339/umbraco`
@@ -105,17 +116,17 @@ Use this for backend development, testing full builds, or when you don't need ho
**Vite Dev Server Mode (Frontend Development with Hot Reload)**
Use this for frontend-only development with hot module reloading:
1. Configure backend for frontend development - Add to `src/Umbraco.Web.UI/appsettings.json` under `Umbraco:CMS:Security`:
```json
"BackOfficeHost": "http://localhost:5173",
"AuthorizeCallbackPathName": "/oauth_complete",
"AuthorizeCallbackLogoutPathName": "/logout",
"AuthorizeCallbackErrorPathName": "/error",
"BackOfficeTokenCookie": {
"Enabled": true,
"SameSite": "None"
}
```
```json
"BackOfficeHost": "http://localhost:5173",
"AuthorizeCallbackPathName": "/oauth_complete",
"AuthorizeCallbackLogoutPathName": "/logout",
"AuthorizeCallbackErrorPathName": "/error",
"BackOfficeTokenCookie": {
"SameSite": "None"
}
```
2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build`
3. Run frontend dev server: `cd src/Umbraco.Web.UI.Client && npm run dev:server`
4. Access backoffice: `http://localhost:5173/` (no `/umbraco` prefix)
@@ -124,39 +135,48 @@ Use this for frontend-only development with hot module reloading:
**Important:** Remove the `BackOfficeHost` configuration before committing or switching back to production mode.
### Backend-Only Development
For backend-only changes, disable frontend builds:
- Comment out the target named "BuildStaticAssetsPreconditions" in src/Umbraco.Cms.StaticAssets.csproj:
```
<!--<Target Name="BuildStaticAssetsPreconditions" BeforeTargets="AssignTargetPaths">
[...]
</Target>-->
```
- Remember to uncomment before committing
- Comment out the target named "BuildStaticAssetsPreconditions" in src/Umbraco.Cms.StaticAssets.csproj:
```
<!--<Target Name="BuildStaticAssetsPreconditions" BeforeTargets="AssignTargetPaths">
[...]
</Target>-->
```
- Remember to uncomment before committing
### Building NuGet Packages
To build custom NuGet packages for testing:
```bash
dotnet pack -c Release -o Build.Out
dotnet nuget add source [Path to Build.Out folder] -n MyLocalFeed
```
### Regenerating Frontend API Types
When changing Management API:
```bash
cd src/Umbraco.Web.UI.Client
npm run generate:server-api-dev
```
Also update OpenApi.json from /umbraco/swagger/management/swagger.json
## Database Setup
Default configuration supports SQLite for development. For production-like testing:
- Use SQL Server/LocalDb for better performance
- Configure connection string in src/Umbraco.Web.UI/appsettings.json
- Use SQL Server/LocalDb for better performance
- Configure connection string in src/Umbraco.Web.UI/appsettings.json
## Clean Up / Reset
To reset development environment:
```bash
# Remove configuration and database
rm src/Umbraco.Web.UI/appsettings.json
@@ -168,31 +188,31 @@ git clean -xdf .
## Version Information
- Target Framework: .NET (version specified in global.json)
- Current Version: (specified in version.json)
- Node.js Requirement: (specified in src/Umbraco.Web.UI.Client/.nvmrc)
- npm Requirement: Latest compatible version
- Target Framework: .NET (version specified in global.json)
- Current Version: (specified in version.json)
- Node.js Requirement: (specified in src/Umbraco.Web.UI.Client/.nvmrc)
- npm Requirement: Latest compatible version
## Known Issues
- Build requires full git history (not shallow clone) due to GitVersioning
- Some NuGet package security warnings are expected (SixLabors.ImageSharp vulnerabilities)
- Frontend tests require Playwright browser installation: `npx playwright install`
- Older Node.js versions may show engine compatibility warnings (check .nvmrc for current requirement)
- Build requires full git history (not shallow clone) due to GitVersioning
- Some NuGet package security warnings are expected (SixLabors.ImageSharp vulnerabilities)
- Frontend tests require Playwright browser installation: `npx playwright install`
- Older Node.js versions may show engine compatibility warnings (check .nvmrc for current requirement)
## Timing Expectations
**NEVER CANCEL** these operations - they are expected to take time:
| Operation | Expected Time | Timeout Setting |
|-----------|--------------|-----------------|
| `dotnet restore` | 50 seconds | 90+ seconds |
| `dotnet build` | 4.5 minutes | 10+ minutes |
| `npm ci` | 11 seconds | 30+ seconds |
| `npm run build:for:cms` | 1.25 minutes | 5+ minutes |
| `npm test` | 2 minutes | 5+ minutes |
| `npm run lint` | 1 minute | 5+ minutes |
| Unit tests | 1 minute | 5+ minutes |
| Integration tests | Variable | 10+ minutes |
| Operation | Expected Time | Timeout Setting |
| ----------------------- | ------------- | --------------- |
| `dotnet restore` | 50 seconds | 90+ seconds |
| `dotnet build` | 4.5 minutes | 10+ minutes |
| `npm ci` | 11 seconds | 30+ seconds |
| `npm run build:for:cms` | 1.25 minutes | 5+ minutes |
| `npm test` | 2 minutes | 5+ minutes |
| `npm run lint` | 1 minute | 5+ minutes |
| Unit tests | 1 minute | 5+ minutes |
| Integration tests | Variable | 10+ minutes |
Always wait for commands to complete rather than canceling and retrying.
Always wait for commands to complete rather than canceling and retrying.

1
.vscode/launch.json vendored
View File

@@ -107,7 +107,6 @@
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKLOGOUTPATHNAME": "/logout",
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKERRORPATHNAME": "/error",
"UMBRACO__CMS__SECURITY__KEEPUSERLOGGEDIN": "true",
"UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__ENABLED": "true",
"UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__SAMESITE": "None"
},
"sourceFileMap": {

View File

@@ -12,27 +12,27 @@
</ItemGroup>
<!-- Microsoft packages -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.0-rc.2.25502.107" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.9.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.0.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
</ItemGroup>
<!-- Umbraco packages -->

View File

@@ -15,6 +15,7 @@ namespace Umbraco.Cms.Api.Common.DependencyInjection;
internal sealed class HideBackOfficeTokensHandler
: IOpenIddictServerHandler<OpenIddictServerEvents.ApplyTokenResponseContext>,
IOpenIddictServerHandler<OpenIddictServerEvents.ApplyAuthorizationResponseContext>,
IOpenIddictServerHandler<OpenIddictServerEvents.ExtractTokenRequestContext>,
IOpenIddictValidationHandler<OpenIddictValidationEvents.ProcessAuthenticationContext>,
INotificationHandler<UserLogoutSuccessNotification>
@@ -22,6 +23,7 @@ internal sealed class HideBackOfficeTokensHandler
private const string RedactedTokenValue = "[redacted]";
private const string AccessTokenCookieKey = "__Host-umbAccessToken";
private const string RefreshTokenCookieKey = "__Host-umbRefreshToken";
private const string PkceCodeCookieKey = "__Host-umbPkceCode";
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IDataProtectionProvider _dataProtectionProvider;
@@ -70,6 +72,28 @@ internal sealed class HideBackOfficeTokensHandler
return ValueTask.CompletedTask;
}
/// <summary>
/// This is invoked when a PKCE code is issued to the client. For the back-office client, we will intercept the
/// response, write the PKCE code from the response into a HTTP-only cookie, and redact the code from the response,
/// so it's not exposed to the client.
/// </summary>
public ValueTask HandleAsync(OpenIddictServerEvents.ApplyAuthorizationResponseContext context)
{
if (context.Request?.ClientId is not Constants.OAuthClientIds.BackOffice)
{
// Only ever handle the back-office client.
return ValueTask.CompletedTask;
}
if (context.Response.Code is not null)
{
SetCookie(GetHttpContext(), PkceCodeCookieKey, context.Response.Code);
context.Response.Code = RedactedTokenValue;
}
return ValueTask.CompletedTask;
}
/// <summary>
/// This is invoked when requesting new tokens.
/// </summary>
@@ -81,7 +105,23 @@ internal sealed class HideBackOfficeTokensHandler
return ValueTask.CompletedTask;
}
// For the back-office client, this only happens when a refresh token is being exchanged for a new access token.
// Handle when the PKCE code is being exchanged for an access token.
if (context.Request.Code == RedactedTokenValue
&& TryGetCookie(PkceCodeCookieKey, out var code))
{
context.Request.Code = code;
// We won't need the PKCE cookie after this, let's remove it.
RemoveCookie(GetHttpContext(), PkceCodeCookieKey);
}
else
{
// PCKE codes should always be redacted. If we got here, someone might be trying to pass another PKCE
// code. For security reasons, explicitly discard the code (if any) to be on the safe side.
context.Request.Code = null;
}
// Handle when a refresh token is being exchanged for a new access token.
if (context.Request.RefreshToken == RedactedTokenValue
&& TryGetCookie(RefreshTokenCookieKey, out var refreshToken))
{
@@ -95,7 +135,6 @@ internal sealed class HideBackOfficeTokensHandler
context.Request.RefreshToken = null;
}
return ValueTask.CompletedTask;
}
@@ -140,7 +179,15 @@ internal sealed class HideBackOfficeTokensHandler
{
var cookieValue = EncryptionHelper.Encrypt(value, _dataProtectionProvider);
var cookieOptions = new CookieOptions
RemoveCookie(httpContext, key);
httpContext.Response.Cookies.Append(key, cookieValue, GetCookieOptions(httpContext));
}
private void RemoveCookie(HttpContext httpContext, string key)
=> httpContext.Response.Cookies.Delete(key, GetCookieOptions(httpContext));
private CookieOptions GetCookieOptions(HttpContext httpContext) =>
new()
{
// Prevent the client-side scripts from accessing the cookie.
HttpOnly = true,
@@ -164,10 +211,6 @@ internal sealed class HideBackOfficeTokensHandler
SameSite = ParseSameSiteMode(_backOfficeTokenCookieSettings.SameSite),
};
httpContext.Response.Cookies.Delete(key, cookieOptions);
httpContext.Response.Cookies.Append(key, cookieValue, cookieOptions);
}
private bool TryGetCookie(string key, [NotNullWhen(true)] out string? value)
{
if (GetHttpContext().Request.Cookies.TryGetValue(key, out var cookieValue))

View File

@@ -120,21 +120,24 @@ public static class UmbracoBuilderAuthExtensions
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
});
if (hideBackOfficeTokens)
options.AddEventHandler<OpenIddictServerEvents.ApplyTokenResponseContext>(configuration =>
{
options.AddEventHandler<OpenIddictServerEvents.ApplyTokenResponseContext>(configuration =>
{
configuration
.UseSingletonHandler<HideBackOfficeTokensHandler>()
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ProcessJsonResponse<OpenIddictServerEvents.ApplyTokenResponseContext>.Descriptor.Order - 1);
});
options.AddEventHandler<OpenIddictServerEvents.ExtractTokenRequestContext>(configuration =>
{
configuration
.UseSingletonHandler<HideBackOfficeTokensHandler>()
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ExtractPostRequest<OpenIddictServerEvents.ExtractTokenRequestContext>.Descriptor.Order + 1);
});
}
configuration
.UseSingletonHandler<HideBackOfficeTokensHandler>()
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ProcessJsonResponse<OpenIddictServerEvents.ApplyTokenResponseContext>.Descriptor.Order - 1);
});
options.AddEventHandler<OpenIddictServerEvents.ApplyAuthorizationResponseContext>(configuration =>
{
configuration
.UseSingletonHandler<HideBackOfficeTokensHandler>()
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.Authentication.ProcessQueryResponse.Descriptor.Order - 1);
});
options.AddEventHandler<OpenIddictServerEvents.ExtractTokenRequestContext>(configuration =>
{
configuration
.UseSingletonHandler<HideBackOfficeTokensHandler>()
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ExtractPostRequest<OpenIddictServerEvents.ExtractTokenRequestContext>.Descriptor.Order + 1);
});
})
// Register the OpenIddict validation components.
@@ -160,24 +163,18 @@ public static class UmbracoBuilderAuthExtensions
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
});
if (hideBackOfficeTokens)
options.AddEventHandler<OpenIddictValidationEvents.ProcessAuthenticationContext>(configuration =>
{
options.AddEventHandler<OpenIddictValidationEvents.ProcessAuthenticationContext>(configuration =>
{
configuration
.UseSingletonHandler<HideBackOfficeTokensHandler>()
// IMPORTANT: the handler must be AFTER the built-in query string handler, because the client-side SignalR library sometimes appends access tokens to the query string.
.SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ExtractAccessTokenFromQueryString.Descriptor.Order + 1);
});
}
configuration
.UseSingletonHandler<HideBackOfficeTokensHandler>()
// IMPORTANT: the handler must be AFTER the built-in query string handler, because the client-side SignalR library sometimes appends access tokens to the query string.
.SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ExtractAccessTokenFromQueryString.Descriptor.Order + 1);
});
});
builder.Services.AddSingleton<IDistributedBackgroundJob, OpenIddictCleanupJob>();
builder.Services.ConfigureOptions<ConfigureOpenIddict>();
if (hideBackOfficeTokens)
{
builder.AddNotificationHandler<UserLogoutSuccessNotification, HideBackOfficeTokensHandler>();
}
builder.AddNotificationHandler<UserLogoutSuccessNotification, HideBackOfficeTokensHandler>();
}
}

View File

@@ -46,7 +46,7 @@ public class NewsDashboardService : INewsDashboardService
/// <inheritdoc />
public async Task<NewsDashboardResponseModel> GetItemsAsync()
{
const string BaseUrl = "https://umbraco-dashboard-news.euwest01.umbraco.io";
const string BaseUrl = "https://news-dashboard.umbraco.com";
const string Path = "/api/News";
var version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild();

View File

@@ -20,7 +20,14 @@ public interface IRepositoryCacheVersionAccessor
/// <returns>
/// The cache version if found, or <see langword="null"/> if the version doesn't exist or the request is a client-side request.
/// </returns>
public Task<RepositoryCacheVersion?> GetAsync(string cacheKey);
Task<RepositoryCacheVersion?> GetAsync(string cacheKey);
/// <summary>
/// Notifies of a version change on a given cache key.
/// </summary>
/// <param name="cacheKey">Key of the changed version.</param>
void VersionChanged(string cacheKey)
{ }
/// <summary>
/// Notifies the accessor that caches have been synchronized.
@@ -29,5 +36,5 @@ public interface IRepositoryCacheVersionAccessor
/// This method is called after cache synchronization to temporarily bypass version checking,
/// preventing recursive sync attempts while repositories reload data from the database.
/// </remarks>
public void CachesSynced();
void CachesSynced();
}

View File

@@ -88,6 +88,7 @@ internal class RepositoryCacheVersionService : IRepositoryCacheVersionService
_logger.LogDebug("Setting cache for {EntityType} to version {Version}", typeof(TEntity).Name, newVersion);
await _repositoryCacheVersionRepository.SaveAsync(new RepositoryCacheVersion { Identifier = cacheKey, Version = newVersion.ToString() });
_cacheVersions[cacheKey] = newVersion;
_repositoryCacheVersionAccessor.VersionChanged(cacheKey);
scope.Complete();
}

View File

@@ -9,17 +9,8 @@ namespace Umbraco.Cms.Core.Configuration.Models;
[Obsolete("This will be replaced with a different authentication scheme. Scheduled for removal in Umbraco 18.")]
public class BackOfficeTokenCookieSettings
{
private const bool StaticEnabled = false;
private const string StaticSameSite = "Strict";
/// <summary>
/// Gets or sets a value indicating whether to enable access and refresh tokens in cookies.
/// </summary>
[DefaultValue(StaticEnabled)]
[Obsolete("This is only configurable in Umbraco 16. Scheduled for removal in Umbraco 17.")]
public bool Enabled { get; set; } = StaticEnabled;
/// <summary>
/// Gets or sets a value indicating whether the cookie SameSite configuration.
/// </summary>

View File

@@ -64,6 +64,10 @@ public static class UdiGetterExtensions
{
entityType = Constants.UdiEntityType.DataTypeContainer;
}
else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentBlueprint)
{
entityType = Constants.UdiEntityType.DocumentBlueprintContainer;
}
else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentType)
{
entityType = Constants.UdiEntityType.DocumentTypeContainer;
@@ -72,9 +76,9 @@ public static class UdiGetterExtensions
{
entityType = Constants.UdiEntityType.MediaTypeContainer;
}
else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentBlueprint)
else if (entity.ContainedObjectType == Constants.ObjectTypes.MemberType)
{
entityType = Constants.UdiEntityType.DocumentBlueprintContainer;
entityType = Constants.UdiEntityType.MemberTypeContainer;
}
else
{

View File

@@ -10,10 +10,10 @@ public sealed class EntityContainer : TreeEntityBase, IUmbracoEntity
private static readonly Dictionary<Guid, Guid> ObjectTypeMap = new()
{
{ Constants.ObjectTypes.DataType, Constants.ObjectTypes.DataTypeContainer },
{ Constants.ObjectTypes.DocumentBlueprint, Constants.ObjectTypes.DocumentBlueprintContainer },
{ Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentTypeContainer },
{ Constants.ObjectTypes.MediaType, Constants.ObjectTypes.MediaTypeContainer },
{ Constants.ObjectTypes.MemberType, Constants.ObjectTypes.MemberTypeContainer },
{ Constants.ObjectTypes.DocumentBlueprint, Constants.ObjectTypes.DocumentBlueprintContainer },
};
/// <summary>
@@ -83,7 +83,7 @@ public sealed class EntityContainer : TreeEntityBase, IUmbracoEntity
public static Guid GetContainedObjectType(Guid containerObjectType)
{
Guid contained = ObjectTypeMap.FirstOrDefault(x => x.Value == containerObjectType).Key;
if (contained == null)
if (contained == default)
{
throw new ArgumentException("Not a container object type.", nameof(containerObjectType));
}

View File

@@ -139,6 +139,7 @@ public class UmbracoPlan : MigrationPlan
To<V_17_0_0.AddDistributedJobLock>("{263075BF-F18A-480D-92B4-4947D2EAB772}");
To<V_17_0_0.AddLastSyncedTable>("26179D88-58CE-4C92-B4A4-3CBA6E7188AC");
To<V_17_0_0.EnsureDefaultMediaFolderHasDefaultCollection>("{8B2C830A-4FFB-4433-8337-8649B0BF52C8}");
To<V_17_0_0.InvalidateBackofficeUserAccess>("{1C38D589-26BB-4A46-9ABE-E4A0DF548A87}");
// To 18.0.0
// TODO (V18): Enable on 18 branch

View File

@@ -0,0 +1,15 @@
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_0_0;
public class InvalidateBackofficeUserAccess : AsyncMigrationBase
{
public InvalidateBackofficeUserAccess(IMigrationContext context)
: base(context)
{
}
protected override Task MigrateAsync()
{
InvalidateBackofficeUserAccess = true;
return Task.CompletedTask;
}
}

View File

@@ -77,5 +77,17 @@ public class RepositoryCacheVersionAccessor : IRepositoryCacheVersionAccessor
return databaseVersion;
}
/// <inheritdoc />
public void VersionChanged(string cacheKey)
{
var removed = _requestCache.Remove(cacheKey);
if (removed is false)
{
_logger.LogDebug("Cache version for key {CacheKey} wasn't removed from request cache, possibly missing HTTP context", cacheKey);
}
}
/// <inheritdoc />
public void CachesSynced() => _requestCache.ClearOfType<RepositoryCacheVersion>();
}

View File

@@ -41,7 +41,6 @@ Open this file in an editor: `/src/Umbraco.Web.UI/appsettings.Development.json`
"AuthorizeCallbackLogoutPathName": "/logout",
"AuthorizeCallbackErrorPathName": "/error",,
"BackOfficeTokenCookie": {
"Enabled": true,
"SameSite": "None"
}
},
@@ -52,7 +51,7 @@ Open this file in an editor: `/src/Umbraco.Web.UI/appsettings.Development.json`
This will override the backoffice host URL, enabling the Client to run from a different origin.
> [!NOTE]
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `BackOfficeTokenCookie` settings are correct. Namely, that `SameSite` should be set to `None` when running the front-end server separately.
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `Umbraco:Cms:Security:BackOfficeTokenCookie:SameSite` setting is set to `None`.
#### 2. Start Umbraco

View File

@@ -53,35 +53,35 @@ const customItems: Array<ExampleCollectionItemModel> = [
unique: '1',
entityType: 'example',
name: 'Example 1',
icon: 'icon-shape-triangle',
icon: 'icon-shape-triangle yellow',
isPickable: true,
},
{
unique: '2',
entityType: 'example',
name: 'Example 2',
icon: 'icon-shape-triangle',
icon: 'icon-shape-triangle yellow',
isPickable: true,
},
{
unique: '3',
entityType: 'example',
name: 'Example 3',
icon: 'icon-shape-triangle',
icon: 'icon-shape-triangle yellow',
isPickable: true,
},
{
unique: '4',
entityType: 'example',
name: 'Example 4',
icon: 'icon-shape-triangle',
icon: 'icon-shape-triangle yellow',
isPickable: false,
},
{
unique: '5',
entityType: 'example',
name: 'Example 5',
icon: 'icon-shape-triangle',
icon: 'icon-shape-triangle yellow',
isPickable: true,
},
];

View File

@@ -19,7 +19,9 @@ import type {
UmbTreeChildrenOfRequestArgs,
UmbTreeRootItemsRequestArgs,
} from '@umbraco-cms/backoffice/tree';
import { getConfigValue, type UmbConfigCollectionModel } from '@umbraco-cms/backoffice/utils';
import { getConfigValue } from '@umbraco-cms/backoffice/utils';
type ExampleDocumentPickerConfigCollectionModel = Array<{ alias: 'filter'; value: string }>;
export class ExampleDocumentPickerPropertyEditorDataSource
extends UmbControllerBase
@@ -30,17 +32,17 @@ export class ExampleDocumentPickerPropertyEditorDataSource
#tree = new UmbDocumentTreeRepository(this);
#item = new UmbDocumentItemRepository(this);
#search = new UmbDocumentSearchRepository(this);
#config: UmbConfigCollectionModel = [];
#config: ExampleDocumentPickerConfigCollectionModel = [];
treePickableFilter: (treeItem: UmbDocumentTreeItemModel) => boolean = (treeItem) => !!treeItem.unique;
setConfig(config: UmbConfigCollectionModel) {
setConfig(config: ExampleDocumentPickerConfigCollectionModel) {
// TODO: add examples for all config options
this.#config = config;
this.#applyPickableFilterFromConfig();
}
getConfig(): UmbConfigCollectionModel {
getConfig(): ExampleDocumentPickerConfigCollectionModel {
return this.#config;
}
@@ -72,7 +74,7 @@ export class ExampleDocumentPickerPropertyEditorDataSource
}
#getAllowedDocumentTypesConfig() {
const filterString = getConfigValue<string>(this.#config, 'filter');
const filterString = getConfigValue(this.#config, 'filter');
const filterArray = filterString ? filterString.split(',') : [];
const allowedContentTypes: UmbDocumentSearchRequestArgs['allowedContentTypes'] = filterArray.map(
(unique: string) => ({

View File

@@ -61,9 +61,7 @@ export class UmbDefaultCollectionMenuItemElement extends UmbLitElement {
?selected=${this._isSelected}
@selected=${() => this.#api?.select()}
@deselected=${() => this.#api?.deselect()}>
${item.icon
? html`<uui-icon slot="icon" name=${item.icon}></uui-icon>`
: html`<uui-icon slot="icon" name=${getItemFallbackIcon()}></uui-icon>`}
<umb-icon slot="icon" name=${item.icon ?? getItemFallbackIcon()}></umb-icon>
</uui-menu-item>
`;
}

View File

@@ -82,6 +82,12 @@ export class UmbPickerInputContext<
getSelection() {
return this.#itemManager.getUniques();
}
getSelectedItems() {
return this.#itemManager.getItems();
}
getSelectedItemByUnique(unique: string) {
return this.#itemManager.getItems().find((item) => item.unique === unique);
}
setSelection(selection: Array<string | null>) {
// Note: Currently we do not support picking root item. So we filter out null values:
@@ -111,21 +117,12 @@ export class UmbPickerInputContext<
this.getHostElement().dispatchEvent(new UmbChangeEvent());
}
/**
* Get the display name for an item to show in the remove confirmation dialog.
* Subclasses can override this to provide custom formatting for missing items.
* @param item - The item to get the display name for, or undefined if not found
* @param unique - The unique identifier of the item
* @returns The display name to show in the dialog
*/
protected getItemDisplayName(item: PickedItemType | undefined, unique: string): string {
return item?.name ?? unique;
protected async _requestItemName(unique: string) {
return this.getSelectedItemByUnique(unique)?.name ?? '#general_notFound';
}
async requestRemoveItem(unique: string) {
const item = this.#itemManager.getItems().find((item) => item.unique === unique);
const name = this.getItemDisplayName(item, unique);
const name = await this._requestItemName(unique);
await umbConfirmModal(this, {
color: 'danger',
headline: `#actions_remove?`,

View File

@@ -0,0 +1,38 @@
import { expect } from '@open-wc/testing';
import { getConfigValue } from './index.js';
describe('getConfigValue', () => {
it('should return the value for a matching alias', () => {
const config = [
{ alias: 'foo', value: 123 },
{ alias: 'bar', value: 'hello' },
];
const result = getConfigValue(config, 'foo');
expect(result).to.equal(123);
});
it('should return undefined if alias is not found', () => {
const config = [
{ alias: 'foo', value: 123 },
{ alias: 'bar', value: 'hello' },
];
const result = getConfigValue(config, 'baz');
expect(result).to.be.undefined;
});
it('should return undefined if config is undefined', () => {
const result = getConfigValue(undefined, 'foo');
expect(result).to.be.undefined;
});
it('should work with different value types', () => {
const config = [
{ alias: 'num', value: 42 },
{ alias: 'str', value: 'test' },
{ alias: 'obj', value: { a: 1 } },
];
expect(getConfigValue(config, 'num')).to.equal(42);
expect(getConfigValue(config, 'str')).to.equal('test');
expect(getConfigValue(config, 'obj')).to.deep.equal({ a: 1 });
});
});

View File

@@ -1,12 +1,14 @@
import type { UmbConfigCollectionModel } from './types.js';
import type { UmbConfigCollectionEntryModel } from './types.js';
/**
* Get a value from a config collection by its alias.
* @param {UmbConfigCollectionModel | undefined} config - The config collection to get the value from.
* @param {string} alias - The alias of the value to get.
* @returns {T | undefined} The value with the specified alias, or undefined if not found or if the config is undefined.
* @param config - The config collection to get the value from.
* @param alias - The alias of the config entry to get the value for.
* @returns The value of the config entry with the specified alias, or undefined if not found.
*/
export function getConfigValue<T>(config: UmbConfigCollectionModel | undefined, alias: string): T | undefined {
const entry = config?.find((entry) => entry.alias === alias);
return entry?.value as T | undefined;
export function getConfigValue<T extends UmbConfigCollectionEntryModel, K extends T['alias']>(
config: T[] | undefined,
alias: K,
) {
return config?.find((entry) => entry.alias === alias)?.value as Extract<T, { alias: K }>['value'] | undefined;
}

View File

@@ -7,6 +7,7 @@ import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbDocumentTypeEntityType } from '@umbraco-cms/backoffice/document-type';
import { UMB_VARIANT_CONTEXT } from '@umbraco-cms/backoffice/variant';
import { UmbDocumentItemDataResolver } from '../../item/index.js';
interface UmbDocumentPickerInputContextOpenArgs {
allowedContentTypes?: Array<{ unique: string; entityType: UmbDocumentTypeEntityType }>;
@@ -56,6 +57,15 @@ export class UmbDocumentPickerInputContext extends UmbPickerInputContext<
await super.openPicker(combinedPickerData);
}
protected override async _requestItemName(unique: string): Promise<string> {
const item = this.getSelectedItemByUnique(unique);
const resolver = new UmbDocumentItemDataResolver(this);
resolver.setData(item);
const name = await resolver.getName();
this.removeUmbController(resolver);
return name ?? '#general_notFound';
}
#pickableFilter = (
item: UmbDocumentItemModel,
allowedContentTypes?: Array<{ unique: string; entityType: UmbDocumentTypeEntityType }>,

View File

@@ -26,8 +26,6 @@
</ItemGroup>
<ItemGroup>
<ClientAssetsInputs Include="Client\**" Exclude="$(DefaultItemExcludes)" />
<!-- Dont include the client folder as part of packaging nuget build -->
<Content Remove="Client\**" />
@@ -39,19 +37,4 @@
<Folder Include="wwwroot\" />
</ItemGroup>
<!-- Restore and build Client files -->
<Target Name="RestoreClient" Inputs="Client\package.json;Client\package-lock.json" Outputs="Client\node_modules\.package-lock.json">
<Message Importance="high" Text="Restoring Client NPM packages..." />
<Exec Command="npm i" WorkingDirectory="Client" />
</Target>
<Target Name="BuildClient" BeforeTargets="AssignTargetPaths" DependsOnTargets="RestoreClient" Inputs="@(ClientAssetsInputs)" Outputs="$(IntermediateOutputPath)client.complete.txt">
<Message Importance="high" Text="Executing Client NPM build script..." />
<Exec Command="npm run build" WorkingDirectory="Client" />
<ItemGroup>
<_ClientAssetsBuildOutput Include="wwwroot\App_Plugins\**" />
</ItemGroup>
<WriteLinesToFile File="$(IntermediateOutputPath)client.complete.txt" Lines="@(_ClientAssetsBuildOutput)" Overwrite="true" />
</Target>
</Project>

View File

@@ -8,7 +8,7 @@
"hasInstallScript": true,
"dependencies": {
"@umbraco/json-models-builders": "^2.0.42",
"@umbraco/playwright-testhelpers": "^17.0.6",
"@umbraco/playwright-testhelpers": "^17.0.8",
"camelize": "^1.0.0",
"dotenv": "^16.3.1",
"node-fetch": "^2.6.7"
@@ -67,9 +67,9 @@
}
},
"node_modules/@umbraco/playwright-testhelpers": {
"version": "17.0.6",
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-17.0.6.tgz",
"integrity": "sha512-M0e5HJCqSTDxORFhebaNNGzBB4v6+77MerK6ctG1f+bU3JHfmbGZr4A4HDkD9eAeU7WGu5q7xoASdI0J1wqb1w==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-17.0.8.tgz",
"integrity": "sha512-LUVBdsweiS0WpE1F9YTQejmSxdtgEvbcmLHX57e2S2AbNkdVuR8cJ0rYd9TqSKtNU8ckwnk6YRtVikegU0D64w==",
"license": "MIT",
"dependencies": {
"@umbraco/json-models-builders": "2.0.42",

View File

@@ -23,7 +23,7 @@
},
"dependencies": {
"@umbraco/json-models-builders": "^2.0.42",
"@umbraco/playwright-testhelpers": "^17.0.6",
"@umbraco/playwright-testhelpers": "^17.0.8",
"camelize": "^1.0.0",
"dotenv": "^16.3.1",
"node-fetch": "^2.6.7"

View File

@@ -194,7 +194,8 @@ test('can add multiple content start nodes for a user', async ({umbracoApi, umbr
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
test('can remove a content start node from a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
// TODO: Look into flaky test
test.fixme('can remove a content start node from a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
// Arrange
const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName);
const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]);

View File

@@ -16,9 +16,10 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions;
public class UdiGetterExtensionsTests
{
[TestCase(Constants.ObjectTypes.Strings.DataType, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://data-type-container/6ad82c70685c4e049b36d81bd779d16f")]
[TestCase(Constants.ObjectTypes.Strings.DocumentBlueprint, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://document-blueprint-container/6ad82c70685c4e049b36d81bd779d16f")]
[TestCase(Constants.ObjectTypes.Strings.DocumentType, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://document-type-container/6ad82c70685c4e049b36d81bd779d16f")]
[TestCase(Constants.ObjectTypes.Strings.MediaType, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://media-type-container/6ad82c70685c4e049b36d81bd779d16f")]
[TestCase(Constants.ObjectTypes.Strings.DocumentBlueprint, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://document-blueprint-container/6ad82c70685c4e049b36d81bd779d16f")]
[TestCase(Constants.ObjectTypes.Strings.MemberType, "6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://member-type-container/6ad82c70685c4e049b36d81bd779d16f")]
public void GetUdiForEntityContainer(Guid containedObjectType, Guid key, string expected)
{
EntityContainer entity = new EntityContainer(containedObjectType)