diff --git a/.github/BUILD.md b/.github/BUILD.md index d0143bfbe2..012afcae86 100644 --- a/.github/BUILD.md +++ b/.github/BUILD.md @@ -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. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 858984a9d3..fba0c7a6bf 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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: - ``` - - ``` -- Remember to uncomment before committing + +- Comment out the target named "BuildStaticAssetsPreconditions" in src/Umbraco.Cms.StaticAssets.csproj: + ``` + + ``` +- 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. \ No newline at end of file +Always wait for commands to complete rather than canceling and retrying. diff --git a/.vscode/launch.json b/.vscode/launch.json index c56f06dc2f..89409b0d61 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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": { diff --git a/Directory.Packages.props b/Directory.Packages.props index d07032a3ff..416a91cec3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,27 +12,27 @@ - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs index 8d1dbd040e..725d613183 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/HideBackOfficeTokensHandler.cs @@ -15,6 +15,7 @@ namespace Umbraco.Cms.Api.Common.DependencyInjection; internal sealed class HideBackOfficeTokensHandler : IOpenIddictServerHandler, + IOpenIddictServerHandler, IOpenIddictServerHandler, IOpenIddictValidationHandler, INotificationHandler @@ -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; } + /// + /// 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. + /// + 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; + } + /// /// This is invoked when requesting new tokens. /// @@ -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)) diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs index 487de19908..75a15813da 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs @@ -120,21 +120,24 @@ public static class UmbracoBuilderAuthExtensions configuration.UseSingletonHandler().SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1); }); - if (hideBackOfficeTokens) + options.AddEventHandler(configuration => { - options.AddEventHandler(configuration => - { - configuration - .UseSingletonHandler() - .SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ProcessJsonResponse.Descriptor.Order - 1); - }); - options.AddEventHandler(configuration => - { - configuration - .UseSingletonHandler() - .SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ExtractPostRequest.Descriptor.Order + 1); - }); - } + configuration + .UseSingletonHandler() + .SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ProcessJsonResponse.Descriptor.Order - 1); + }); + options.AddEventHandler(configuration => + { + configuration + .UseSingletonHandler() + .SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.Authentication.ProcessQueryResponse.Descriptor.Order - 1); + }); + options.AddEventHandler(configuration => + { + configuration + .UseSingletonHandler() + .SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ExtractPostRequest.Descriptor.Order + 1); + }); }) // Register the OpenIddict validation components. @@ -160,24 +163,18 @@ public static class UmbracoBuilderAuthExtensions configuration.UseSingletonHandler().SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1); }); - if (hideBackOfficeTokens) + options.AddEventHandler(configuration => { - options.AddEventHandler(configuration => - { - configuration - .UseSingletonHandler() - // 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() + // 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(); builder.Services.ConfigureOptions(); - if (hideBackOfficeTokens) - { - builder.AddNotificationHandler(); - } + builder.AddNotificationHandler(); } } diff --git a/src/Umbraco.Cms.Api.Management/Services/NewsDashboard/NewsDashboardService.cs b/src/Umbraco.Cms.Api.Management/Services/NewsDashboard/NewsDashboardService.cs index 2d05e20df3..6dfe7fde54 100644 --- a/src/Umbraco.Cms.Api.Management/Services/NewsDashboard/NewsDashboardService.cs +++ b/src/Umbraco.Cms.Api.Management/Services/NewsDashboard/NewsDashboardService.cs @@ -46,7 +46,7 @@ public class NewsDashboardService : INewsDashboardService /// public async Task 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(); diff --git a/src/Umbraco.Core/Cache/IRepositoryCacheVersionAccessor.cs b/src/Umbraco.Core/Cache/IRepositoryCacheVersionAccessor.cs index 61ae733fe1..af304dc19e 100644 --- a/src/Umbraco.Core/Cache/IRepositoryCacheVersionAccessor.cs +++ b/src/Umbraco.Core/Cache/IRepositoryCacheVersionAccessor.cs @@ -20,7 +20,14 @@ public interface IRepositoryCacheVersionAccessor /// /// The cache version if found, or if the version doesn't exist or the request is a client-side request. /// - public Task GetAsync(string cacheKey); + Task GetAsync(string cacheKey); + + /// + /// Notifies of a version change on a given cache key. + /// + /// Key of the changed version. + void VersionChanged(string cacheKey) + { } /// /// 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. /// - public void CachesSynced(); + void CachesSynced(); } diff --git a/src/Umbraco.Core/Cache/RepositoryCacheVersionService.cs b/src/Umbraco.Core/Cache/RepositoryCacheVersionService.cs index 541e9f032e..09269f92e1 100644 --- a/src/Umbraco.Core/Cache/RepositoryCacheVersionService.cs +++ b/src/Umbraco.Core/Cache/RepositoryCacheVersionService.cs @@ -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(); } diff --git a/src/Umbraco.Core/Configuration/Models/BackOfficeTokenCookieSettings.cs b/src/Umbraco.Core/Configuration/Models/BackOfficeTokenCookieSettings.cs index 4019c42547..e7632d4270 100644 --- a/src/Umbraco.Core/Configuration/Models/BackOfficeTokenCookieSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/BackOfficeTokenCookieSettings.cs @@ -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"; - /// - /// Gets or sets a value indicating whether to enable access and refresh tokens in cookies. - /// - [DefaultValue(StaticEnabled)] - [Obsolete("This is only configurable in Umbraco 16. Scheduled for removal in Umbraco 17.")] - public bool Enabled { get; set; } = StaticEnabled; - /// /// Gets or sets a value indicating whether the cookie SameSite configuration. /// diff --git a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs index a5adc0de2a..763dde8410 100644 --- a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs +++ b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs @@ -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 { diff --git a/src/Umbraco.Core/Models/EntityContainer.cs b/src/Umbraco.Core/Models/EntityContainer.cs index b6770913a6..02cefdc795 100644 --- a/src/Umbraco.Core/Models/EntityContainer.cs +++ b/src/Umbraco.Core/Models/EntityContainer.cs @@ -10,10 +10,10 @@ public sealed class EntityContainer : TreeEntityBase, IUmbracoEntity private static readonly Dictionary 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 }, }; /// @@ -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)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 8bdddc0a55..3e0ecb7f94 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -139,6 +139,7 @@ public class UmbracoPlan : MigrationPlan To("{263075BF-F18A-480D-92B4-4947D2EAB772}"); To("26179D88-58CE-4C92-B4A4-3CBA6E7188AC"); To("{8B2C830A-4FFB-4433-8337-8649B0BF52C8}"); + To("{1C38D589-26BB-4A46-9ABE-E4A0DF548A87}"); // To 18.0.0 // TODO (V18): Enable on 18 branch diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/InvalidateBackofficeUserAccess.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/InvalidateBackofficeUserAccess.cs new file mode 100644 index 0000000000..fdb111df1a --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/InvalidateBackofficeUserAccess.cs @@ -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; + } +} diff --git a/src/Umbraco.Web.Common/Cache/RepositoryCacheVersionAccessor.cs b/src/Umbraco.Web.Common/Cache/RepositoryCacheVersionAccessor.cs index b96e2546e1..4485c80834 100644 --- a/src/Umbraco.Web.Common/Cache/RepositoryCacheVersionAccessor.cs +++ b/src/Umbraco.Web.Common/Cache/RepositoryCacheVersionAccessor.cs @@ -77,5 +77,17 @@ public class RepositoryCacheVersionAccessor : IRepositoryCacheVersionAccessor return databaseVersion; } + + /// + 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); + } + } + + /// public void CachesSynced() => _requestCache.ClearOfType(); } diff --git a/src/Umbraco.Web.UI.Client/.github/README.md b/src/Umbraco.Web.UI.Client/.github/README.md index d677ad9e0d..fc94ba5835 100644 --- a/src/Umbraco.Web.UI.Client/.github/README.md +++ b/src/Umbraco.Web.UI.Client/.github/README.md @@ -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 diff --git a/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-collection-data-source.ts b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-collection-data-source.ts index 34328f20b7..e0b5b84d7a 100644 --- a/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-collection-data-source.ts +++ b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-collection-data-source.ts @@ -53,35 +53,35 @@ const customItems: Array = [ 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, }, ]; diff --git a/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-document-picker-data-source.ts b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-document-picker-data-source.ts index 47cfbdc043..3ba6c2d2ef 100644 --- a/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-document-picker-data-source.ts +++ b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-document-picker-data-source.ts @@ -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(this.#config, 'filter'); + const filterString = getConfigValue(this.#config, 'filter'); const filterArray = filterString ? filterString.split(',') : []; const allowedContentTypes: UmbDocumentSearchRequestArgs['allowedContentTypes'] = filterArray.map( (unique: string) => ({ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.element.ts index 3dfed63ced..fc30554826 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/menu-item/default/default-collection-menu-item.element.ts @@ -61,9 +61,7 @@ export class UmbDefaultCollectionMenuItemElement extends UmbLitElement { ?selected=${this._isSelected} @selected=${() => this.#api?.select()} @deselected=${() => this.#api?.deselect()}> - ${item.icon - ? html`` - : html``} + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts index 9c90d216c0..31c4649aa5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts @@ -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) { // 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?`, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/config-collection/get-config-value.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/config-collection/get-config-value.test.ts new file mode 100644 index 0000000000..9c510c09b3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/config-collection/get-config-value.test.ts @@ -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 }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/config-collection/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/config-collection/index.ts index b4f03f1155..a2de3f4899 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/config-collection/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/config-collection/index.ts @@ -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(config: UmbConfigCollectionModel | undefined, alias: string): T | undefined { - const entry = config?.find((entry) => entry.alias === alias); - return entry?.value as T | undefined; +export function getConfigValue( + config: T[] | undefined, + alias: K, +) { + return config?.find((entry) => entry.alias === alias)?.value as Extract['value'] | undefined; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts index 1d815ed740..43ac95bd8b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts @@ -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 { + 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 }>, diff --git a/templates/UmbracoExtension/Umbraco.Extension.csproj b/templates/UmbracoExtension/Umbraco.Extension.csproj index 4bc359eaa8..1e515aebbf 100644 --- a/templates/UmbracoExtension/Umbraco.Extension.csproj +++ b/templates/UmbracoExtension/Umbraco.Extension.csproj @@ -26,8 +26,6 @@ - - @@ -39,19 +37,4 @@ - - - - - - - - - - - <_ClientAssetsBuildOutput Include="wwwroot\App_Plugins\**" /> - - - - diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 509985c5b1..55ac1a3b7e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -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", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 7528914b7d..9cbb0c6fc7 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -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" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts index 0ede3be26b..c8024ba19c 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts @@ -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]); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs index 476fefec66..0dd3568175 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs @@ -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)